diff --git a/.github/scripts/build_test.sh b/.github/scripts/build_test.sh index b3d8eb4bf..56ccd6c1a 100755 --- a/.github/scripts/build_test.sh +++ b/.github/scripts/build_test.sh @@ -7,9 +7,16 @@ sleep 30 if kill -s 0 $PID then - echo "Bazarr is still running. We'll kill it..." - kill $PID - exit 0 + echo "Bazarr is still running. We'll test if UI is working..." else exit 1 -fi \ No newline at end of file +fi + +exitcode=0 +curl -fsSL --retry-all-errors --retry 60 --retry-max-time 120 --max-time 10 "http://127.0.0.1:6767" --output /dev/null || exitcode=$? +[[ ${exitcode} == 0 ]] && echo "UI is responsive, good news!" || echo "Oops, UI isn't reachable, bad news..." + +echo "Let's stop Bazarr before we exit..." +pkill -INT -P $PID + +exit ${exitcode} \ No newline at end of file diff --git a/.github/scripts/create_changelog.sh b/.github/scripts/create_changelog.sh index 9b2731590..c9360d617 100755 --- a/.github/scripts/create_changelog.sh +++ b/.github/scripts/create_changelog.sh @@ -10,5 +10,5 @@ latest_verion=$(git describe --tags --abbrev=0) if [[ $RELEASE_MASTER -eq 1 ]]; then auto-changelog --stdout -t changelog-master.hbs --starting-version "$master_version" --commit-limit 3 else - auto-changelog --stdout --starting-version "$latest_verion" --unreleased-only --commit-limit 0 + auto-changelog --stdout --starting-version "$latest_verion" --unreleased-only --commit-limit false fi \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79b1fc149..c7aebfac4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - name: Setup NodeJS uses: actions/setup-node@v4 with: - node-version: "lts/*" + node-version-file: "${{ env.UI_DIRECTORY }}/.nvmrc" - name: Install dependencies run: npm install diff --git a/.github/workflows/release_beta_to_dev.yaml b/.github/workflows/release_beta_to_dev.yaml index f7155984c..d5579f70a 100644 --- a/.github/workflows/release_beta_to_dev.yaml +++ b/.github/workflows/release_beta_to_dev.yaml @@ -38,7 +38,7 @@ jobs: - name: Setup NodeJS uses: actions/setup-node@v4 with: - node-version: "lts/*" + node-version-file: "${{ env.UI_DIRECTORY }}/.nvmrc" - name: Install Global Tools run: npm install -g release-it auto-changelog diff --git a/.github/workflows/release_dev_to_master.yaml b/.github/workflows/release_dev_to_master.yaml index dddf603b0..13eaefee7 100644 --- a/.github/workflows/release_dev_to_master.yaml +++ b/.github/workflows/release_dev_to_master.yaml @@ -40,7 +40,7 @@ jobs: - name: Setup NodeJS uses: actions/setup-node@v4 with: - node-version: "lts/*" + node-version-file: "${{ env.UI_DIRECTORY }}/.nvmrc" - name: Install Global Tools run: npm install -g release-it auto-changelog diff --git a/.github/workflows/test_bazarr_execution.yml b/.github/workflows/test_bazarr_execution.yml index 034eb782a..2ef1a8801 100644 --- a/.github/workflows/test_bazarr_execution.yml +++ b/.github/workflows/test_bazarr_execution.yml @@ -24,7 +24,7 @@ jobs: - name: Setup NodeJS uses: actions/setup-node@v4 with: - node-version: "lts/*" + node-version-file: "${{ env.UI_DIRECTORY }}/.nvmrc" - name: Install UI Dependencies run: npm install diff --git a/bazarr/api/subtitles/subtitles.py b/bazarr/api/subtitles/subtitles.py index a83da76eb..06c5f0d77 100644 --- a/bazarr/api/subtitles/subtitles.py +++ b/bazarr/api/subtitles/subtitles.py @@ -114,6 +114,8 @@ class Subtitles(Resource): subtitles_path = args.get('path') media_type = args.get('type') id = args.get('id') + forced = True if args.get('forced') == 'True' else False + hi = True if args.get('hi') == 'True' else False if not os.path.exists(subtitles_path): return 'Subtitles file not found. Path mapping issue?', 500 @@ -144,6 +146,8 @@ class Subtitles(Resource): 'video_path': video_path, 'srt_path': subtitles_path, 'srt_lang': language, + 'hi': hi, + 'forced': forced, 'reference': args.get('reference') if args.get('reference') not in empty_values else video_path, 'max_offset_seconds': args.get('max_offset_seconds') if args.get('max_offset_seconds') not in empty_values else str(settings.subsync.max_offset_seconds), @@ -167,8 +171,6 @@ class Subtitles(Resource): elif action == 'translate': from_language = subtitles_lang_from_filename(subtitles_path) dest_language = language - forced = True if args.get('forced') == 'true' else False - hi = True if args.get('hi') == 'true' else False try: translate_subtitles_file(video_path=video_path, source_srt_file=subtitles_path, from_lang=from_language, to_lang=dest_language, forced=forced, hi=hi, diff --git a/bazarr/api/system/searches.py b/bazarr/api/system/searches.py index 755711446..a5a3a4960 100644 --- a/bazarr/api/system/searches.py +++ b/bazarr/api/system/searches.py @@ -8,6 +8,8 @@ from app.database import TableShows, TableMovies, database, select from ..utils import authenticate +import textdistance + api_ns_system_searches = Namespace('System Searches', description='Search for series or movies by name') @@ -61,4 +63,6 @@ class Searches(Resource): results.append(result) + # sort results by how closely they match the query + results = sorted(results, key=lambda x: textdistance.hamming.distance(query, x['title'])) return results diff --git a/bazarr/api/system/settings.py b/bazarr/api/system/settings.py index 103df6304..126e2f7a5 100644 --- a/bazarr/api/system/settings.py +++ b/bazarr/api/system/settings.py @@ -73,6 +73,7 @@ class SystemSettings(Resource): mustNotContain=str(item['mustNotContain']), originalFormat=int(item['originalFormat']) if item['originalFormat'] not in None_Keys else None, + tag=item['tag'] if 'tag' in item else None, ) .where(TableLanguagesProfiles.profileId == item['profileId'])) existing.remove(item['profileId']) @@ -89,6 +90,7 @@ class SystemSettings(Resource): mustNotContain=str(item['mustNotContain']), originalFormat=int(item['originalFormat']) if item['originalFormat'] not in None_Keys else None, + tag=item['tag'] if 'tag' in item else None, )) for profileId in existing: # Remove deleted profiles diff --git a/bazarr/app/config.py b/bazarr/app/config.py index 2af614909..15e9959f6 100644 --- a/bazarr/app/config.py +++ b/bazarr/app/config.py @@ -31,12 +31,20 @@ def base_url_slash_cleaner(uri): def validate_ip_address(ip_string): + if ip_string == '*': + return True try: ip_address(ip_string) return True except ValueError: return False +def validate_tags(tags): + if not tags: + return True + + return all(re.match( r'^[a-z0-9_-]+$', item) for item in tags) + ONE_HUNDRED_YEARS_IN_MINUTES = 52560000 ONE_HUNDRED_YEARS_IN_HOURS = 876000 @@ -67,7 +75,7 @@ validators = [ # general section Validator('general.flask_secret_key', must_exist=True, default=hexlify(os.urandom(16)).decode(), is_type_of=str), - Validator('general.ip', must_exist=True, default='0.0.0.0', is_type_of=str, condition=validate_ip_address), + Validator('general.ip', must_exist=True, default='*', is_type_of=str, condition=validate_ip_address), Validator('general.port', must_exist=True, default=6767, is_type_of=int, gte=1, lte=65535), Validator('general.base_url', must_exist=True, default='', is_type_of=str), Validator('general.path_mappings', must_exist=True, default=[], is_type_of=list), @@ -88,6 +96,9 @@ validators = [ Validator('general.use_sonarr', must_exist=True, default=False, is_type_of=bool), Validator('general.use_radarr', must_exist=True, default=False, is_type_of=bool), Validator('general.path_mappings_movie', must_exist=True, default=[], is_type_of=list), + Validator('general.serie_tag_enabled', must_exist=True, default=False, is_type_of=bool), + Validator('general.movie_tag_enabled', must_exist=True, default=False, is_type_of=bool), + Validator('general.remove_profile_tags', must_exist=True, default=[], is_type_of=list, condition=validate_tags), Validator('general.serie_default_enabled', must_exist=True, default=False, is_type_of=bool), Validator('general.serie_default_profile', must_exist=True, default='', is_type_of=(int, str)), Validator('general.movie_default_enabled', must_exist=True, default=False, is_type_of=bool), @@ -176,7 +187,7 @@ validators = [ Validator('sonarr.only_monitored', must_exist=True, default=False, is_type_of=bool), Validator('sonarr.series_sync', must_exist=True, default=60, is_type_of=int, is_in=[15, 60, 180, 360, 720, 1440, 10080, ONE_HUNDRED_YEARS_IN_MINUTES]), - Validator('sonarr.excluded_tags', must_exist=True, default=[], is_type_of=list), + Validator('sonarr.excluded_tags', must_exist=True, default=[], is_type_of=list, condition=validate_tags), Validator('sonarr.excluded_series_types', must_exist=True, default=[], is_type_of=list), Validator('sonarr.use_ffprobe_cache', must_exist=True, default=True, is_type_of=bool), Validator('sonarr.exclude_season_zero', must_exist=True, default=False, is_type_of=bool), @@ -199,7 +210,7 @@ validators = [ Validator('radarr.only_monitored', must_exist=True, default=False, is_type_of=bool), Validator('radarr.movies_sync', must_exist=True, default=60, is_type_of=int, is_in=[15, 60, 180, 360, 720, 1440, 10080, ONE_HUNDRED_YEARS_IN_MINUTES]), - Validator('radarr.excluded_tags', must_exist=True, default=[], is_type_of=list), + Validator('radarr.excluded_tags', must_exist=True, default=[], is_type_of=list, condition=validate_tags), Validator('radarr.use_ffprobe_cache', must_exist=True, default=True, is_type_of=bool), Validator('radarr.defer_search_signalr', must_exist=True, default=False, is_type_of=bool), Validator('radarr.sync_only_monitored_movies', must_exist=True, default=False, is_type_of=bool), @@ -271,6 +282,10 @@ validators = [ Validator('legendasdivx.password', must_exist=True, default='', is_type_of=str, cast=str), Validator('legendasdivx.skip_wrong_fps', must_exist=True, default=False, is_type_of=bool), + # legendasnet section + Validator('legendasnet.username', must_exist=True, default='', is_type_of=str, cast=str), + Validator('legendasnet.password', must_exist=True, default='', is_type_of=str, cast=str), + # ktuvit section Validator('ktuvit.email', must_exist=True, default='', is_type_of=str), Validator('ktuvit.hashed_password', must_exist=True, default='', is_type_of=str, cast=str), @@ -298,6 +313,12 @@ validators = [ # analytics section Validator('analytics.enabled', must_exist=True, default=True, is_type_of=bool), + + # jimaku section + Validator('jimaku.api_key', must_exist=True, default='', is_type_of=str), + Validator('jimaku.enable_name_search_fallback', must_exist=True, default=True, is_type_of=bool), + Validator('jimaku.enable_archives_download', must_exist=True, default=False, is_type_of=bool), + Validator('jimaku.enable_ai_subs', must_exist=True, default=False, is_type_of=bool), # titlovi section Validator('titlovi.username', must_exist=True, default='', is_type_of=str, cast=str), @@ -321,6 +342,9 @@ validators = [ Validator('karagarga.f_username', must_exist=True, default='', is_type_of=str, cast=str), Validator('karagarga.f_password', must_exist=True, default='', is_type_of=str, cast=str), + # subdl section + Validator('subdl.api_key', must_exist=True, default='', is_type_of=str, cast=str), + # subsync section Validator('subsync.use_subsync', must_exist=True, default=False, is_type_of=bool), Validator('subsync.use_subsync_threshold', must_exist=True, default=False, is_type_of=bool), @@ -451,6 +475,7 @@ array_keys = ['excluded_tags', 'enabled_integrations', 'path_mappings', 'path_mappings_movie', + 'remove_profile_tags', 'language_equals', 'blacklisted_languages', 'blacklisted_providers'] diff --git a/bazarr/app/database.py b/bazarr/app/database.py index c2a97987d..3780befea 100644 --- a/bazarr/app/database.py +++ b/bazarr/app/database.py @@ -172,6 +172,7 @@ class TableHistory(Base): video_path = mapped_column(Text) matched = mapped_column(Text) not_matched = mapped_column(Text) + upgradedFromId = mapped_column(Integer, ForeignKey('table_history.id')) class TableHistoryMovie(Base): @@ -190,6 +191,7 @@ class TableHistoryMovie(Base): video_path = mapped_column(Text) matched = mapped_column(Text) not_matched = mapped_column(Text) + upgradedFromId = mapped_column(Integer, ForeignKey('table_history_movie.id')) class TableLanguagesProfiles(Base): @@ -202,6 +204,7 @@ class TableLanguagesProfiles(Base): name = mapped_column(Text, nullable=False) mustContain = mapped_column(Text) mustNotContain = mapped_column(Text) + tag = mapped_column(Text) class TableMovies(Base): @@ -376,6 +379,7 @@ def update_profile_id_list(): 'mustContain': ast.literal_eval(x.mustContain) if x.mustContain else [], 'mustNotContain': ast.literal_eval(x.mustNotContain) if x.mustNotContain else [], 'originalFormat': x.originalFormat, + 'tag': x.tag, } for x in database.execute( select(TableLanguagesProfiles.profileId, TableLanguagesProfiles.name, @@ -383,7 +387,8 @@ def update_profile_id_list(): TableLanguagesProfiles.items, TableLanguagesProfiles.mustContain, TableLanguagesProfiles.mustNotContain, - TableLanguagesProfiles.originalFormat)) + TableLanguagesProfiles.originalFormat, + TableLanguagesProfiles.tag)) .all() ] @@ -418,7 +423,7 @@ def get_profile_cutoff(profile_id): if profile_id and profile_id != 'null': cutoff_language = [] for profile in profile_id_list: - profileId, name, cutoff, items, mustContain, mustNotContain, originalFormat = profile.values() + profileId, name, cutoff, items, mustContain, mustNotContain, originalFormat, tag = profile.values() if cutoff: if profileId == int(profile_id): for item in items: @@ -497,3 +502,29 @@ def convert_list_to_clause(arr: list): return f"({','.join(str(x) for x in arr)})" else: return "" + + +def upgrade_languages_profile_hi_values(): + for languages_profile in (database.execute( + select( + TableLanguagesProfiles.profileId, + TableLanguagesProfiles.name, + TableLanguagesProfiles.cutoff, + TableLanguagesProfiles.items, + TableLanguagesProfiles.mustContain, + TableLanguagesProfiles.mustNotContain, + TableLanguagesProfiles.originalFormat, + TableLanguagesProfiles.tag) + ))\ + .all(): + items = json.loads(languages_profile.items) + for language in items: + if language['hi'] == "only": + language['hi'] = "True" + elif language['hi'] in ["also", "never"]: + language['hi'] = "False" + database.execute( + update(TableLanguagesProfiles) + .values({"items": json.dumps(items)}) + .where(TableLanguagesProfiles.profileId == languages_profile.profileId) + ) diff --git a/bazarr/app/get_providers.py b/bazarr/app/get_providers.py index bc251a4e1..125b24d40 100644 --- a/bazarr/app/get_providers.py +++ b/bazarr/app/get_providers.py @@ -264,6 +264,10 @@ def get_providers_auth(): 'password': settings.legendasdivx.password, 'skip_wrong_fps': settings.legendasdivx.skip_wrong_fps, }, + 'legendasnet': { + 'username': settings.legendasnet.username, + 'password': settings.legendasnet.password, + }, 'xsubs': { 'username': settings.xsubs.username, 'password': settings.xsubs.password, @@ -285,6 +289,12 @@ def get_providers_auth(): 'username': settings.titlovi.username, 'password': settings.titlovi.password, }, + 'jimaku': { + 'api_key': settings.jimaku.api_key, + 'enable_name_search_fallback': settings.jimaku.enable_name_search_fallback, + 'enable_archives_download': settings.jimaku.enable_archives_download, + 'enable_ai_subs': settings.jimaku.enable_ai_subs, + }, 'ktuvit': { 'email': settings.ktuvit.email, 'hashed_password': settings.ktuvit.hashed_password, @@ -322,6 +332,9 @@ def get_providers_auth(): }, "animetosho": { 'search_threshold': settings.animetosho.search_threshold, + }, + "subdl": { + 'api_key': settings.subdl.api_key, } } diff --git a/bazarr/app/logger.py b/bazarr/app/logger.py index a47acf3dc..8803ad3cb 100644 --- a/bazarr/app/logger.py +++ b/bazarr/app/logger.py @@ -58,10 +58,13 @@ class NoExceptionFormatter(logging.Formatter): class UnwantedWaitressMessageFilter(logging.Filter): def filter(self, record): - if settings.general.debug: - # no filtering in debug mode + if settings.general.debug or "BAZARR" in record.msg: + # no filtering in debug mode or if originating from us return True + if record.level != loggin.ERROR: + return False + unwantedMessages = [ "Exception while serving /api/socket.io/", ['Session is disconnected', 'Session not found'], @@ -161,7 +164,7 @@ def configure_logging(debug=False): logging.getLogger("websocket").setLevel(logging.CRITICAL) logging.getLogger("ga4mp.ga4mp").setLevel(logging.ERROR) - logging.getLogger("waitress").setLevel(logging.ERROR) + logging.getLogger("waitress").setLevel(logging.INFO) logging.getLogger("waitress").addFilter(UnwantedWaitressMessageFilter()) logging.getLogger("knowit").setLevel(logging.CRITICAL) logging.getLogger("enzyme").setLevel(logging.CRITICAL) diff --git a/bazarr/app/server.py b/bazarr/app/server.py index 1def54dab..4ffe436ce 100644 --- a/bazarr/app/server.py +++ b/bazarr/app/server.py @@ -50,7 +50,7 @@ class Server: self.connected = True except OSError as error: if error.errno == errno.EADDRNOTAVAIL: - logging.exception("BAZARR cannot bind to specified IP, trying with default (0.0.0.0)") + logging.exception("BAZARR cannot bind to specified IP, trying with 0.0.0.0") self.address = '0.0.0.0' self.connected = False super(Server, self).__init__() @@ -76,8 +76,7 @@ class Server: self.shutdown(EXIT_INTERRUPT) def start(self): - logging.info(f'BAZARR is started and waiting for request on http://{self.server.effective_host}:' - f'{self.server.effective_port}') + self.server.print_listen("BAZARR is started and waiting for requests on: http://{}:{}") signal.signal(signal.SIGINT, self.interrupt_handler) try: self.server.run() diff --git a/bazarr/app/ui.py b/bazarr/app/ui.py index df43f7b0c..79649e30e 100644 --- a/bazarr/app/ui.py +++ b/bazarr/app/ui.py @@ -20,9 +20,10 @@ from .config import settings, base_url from .database import System from .get_args import args +frontend_build_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'frontend', 'build') + ui_bp = Blueprint('ui', __name__, - template_folder=os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), - 'frontend', 'build'), + template_folder=frontend_build_path, static_folder=os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'frontend', 'build', 'assets'), static_url_path='/assets') @@ -38,13 +39,15 @@ static_bp = Blueprint('images', __name__, static_folder=static_directory, static ui_bp.register_blueprint(static_bp) - mimetypes.add_type('application/javascript', '.js') mimetypes.add_type('text/css', '.css') mimetypes.add_type('font/woff2', '.woff2') mimetypes.add_type('image/svg+xml', '.svg') mimetypes.add_type('image/png', '.png') mimetypes.add_type('image/x-icon', '.ico') +mimetypes.add_type('application/manifest+json', '.webmanifest') + +pwa_assets = ['registerSW.js', 'manifest.webmanifest', 'sw.js'] def check_login(actual_method): @@ -70,6 +73,10 @@ def catch_all(path): # login page has been accessed when no authentication is enabled return redirect(base_url or "/", code=302) + # PWA Assets are returned from frontend root folder + if path in pwa_assets or path.startswith('workbox-'): + return send_file(os.path.join(frontend_build_path, path)) + auth = True if settings.auth.type == 'basic': auth = request.authorization @@ -153,8 +160,8 @@ def backup_download(filename): def swaggerui_static(filename): basepath = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'libs', 'flask_restx', 'static') - fullpath = os.path.join(basepath, filename) - if not fullpath.startswith(basepath): + fullpath = os.path.realpath(os.path.join(basepath, filename)) + if not basepath == os.path.commonpath((basepath, fullpath)): return '', 404 else: return send_file(fullpath) @@ -186,7 +193,8 @@ def proxy(protocol, url): elif result.status_code == 401: return dict(status=False, error='Access Denied. Check API key.', code=result.status_code) elif result.status_code == 404: - return dict(status=False, error='Cannot get version. Maybe unsupported legacy API call?', code=result.status_code) + return dict(status=False, error='Cannot get version. Maybe unsupported legacy API call?', + code=result.status_code) elif 300 <= result.status_code <= 399: return dict(status=False, error='Wrong URL Base.', code=result.status_code) else: diff --git a/bazarr/languages/custom_lang.py b/bazarr/languages/custom_lang.py index bc50a4758..e6f3aa2f3 100644 --- a/bazarr/languages/custom_lang.py +++ b/bazarr/languages/custom_lang.py @@ -5,7 +5,8 @@ import os from subzero.language import Language -from app.database import database, insert +from app.database import database, insert, update +from sqlalchemy.exc import IntegrityError logger = logging.getLogger(__name__) @@ -18,7 +19,7 @@ class CustomLanguage: language = "pt-BR" official_alpha2 = "pt" official_alpha3 = "por" - name = "Brazilian Portuguese" + name = "Portuguese (Brazil)" iso = "BR" _scripts = [] _possible_matches = ("pt-br", "pob", "pb", "brazilian", "brasil", "brazil") @@ -50,13 +51,19 @@ class CustomLanguage: """Register the custom language subclasses in the database.""" for sub in cls.__subclasses__(): - database.execute( - insert(table) - .values(code3=sub.alpha3, - code2=sub.alpha2, - name=sub.name, - enabled=0) - .on_conflict_do_nothing()) + try: + database.execute( + insert(table) + .values(code3=sub.alpha3, + code2=sub.alpha2, + name=sub.name, + enabled=0)) + except IntegrityError: + database.execute( + update(table) + .values(code2=sub.alpha2, + name=sub.name) + .where(table.code3 == sub.alpha3)) @classmethod def found_external(cls, subtitle, subtitle_path): @@ -212,7 +219,7 @@ class LatinAmericanSpanish(CustomLanguage): language = "es-MX" official_alpha2 = "es" official_alpha3 = "spa" - name = "Latin American Spanish" + name = "Spanish (Latino)" iso = "MX" # Not fair, but ok _scripts = ("419",) _possible_matches = ( diff --git a/bazarr/languages/get_languages.py b/bazarr/languages/get_languages.py index 2fbd52c89..494343f6f 100644 --- a/bazarr/languages/get_languages.py +++ b/bazarr/languages/get_languages.py @@ -44,6 +44,12 @@ def create_languages_dict(): .values(name='Chinese Simplified') .where(TableSettingsLanguages.code3 == 'zho')) + # replace Modern Greek by Greek to match Sonarr and Radarr languages + database.execute( + update(TableSettingsLanguages) + .values(name='Greek') + .where(TableSettingsLanguages.code3 == 'ell')) + languages_dict = [{ 'code3': x.code3, 'code2': x.code2, @@ -55,6 +61,19 @@ def create_languages_dict(): .all()] +def audio_language_from_name(lang): + lang_map = { + 'Chinese': 'zh', + } + + alpha2_code = lang_map.get(lang, None) + + if alpha2_code is None: + return lang + + return language_from_alpha2(alpha2_code) + + def language_from_alpha2(lang): return next((item['name'] for item in languages_dict if item['code2'] == lang[:2]), None) diff --git a/bazarr/main.py b/bazarr/main.py index ab25f6ae5..8fe9a43fd 100644 --- a/bazarr/main.py +++ b/bazarr/main.py @@ -35,7 +35,7 @@ else: # there's missing embedded packages after a commit check_if_new_update() -from app.database import System, database, update, migrate_db, create_db_revision # noqa E402 +from app.database import System, database, update, migrate_db, create_db_revision, upgrade_languages_profile_hi_values # noqa E402 from app.notifier import update_notifier # noqa E402 from languages.get_languages import load_language_in_db # noqa E402 from app.signalr_client import sonarr_signalr_client, radarr_signalr_client # noqa E402 @@ -49,6 +49,7 @@ if args.create_db_revision: stop_bazarr(EXIT_NORMAL) else: migrate_db(app) + upgrade_languages_profile_hi_values() configure_proxy_func() diff --git a/bazarr/radarr/sync/movies.py b/bazarr/radarr/sync/movies.py index 82416cffb..a5ecf0416 100644 --- a/bazarr/radarr/sync/movies.py +++ b/bazarr/radarr/sync/movies.py @@ -28,6 +28,11 @@ def trace(message): logging.debug(FEATURE_PREFIX + message) +def get_language_profiles(): + return database.execute( + select(TableLanguagesProfiles.profileId, TableLanguagesProfiles.name, TableLanguagesProfiles.tag)).all() + + def update_all_movies(): movies_full_scan_subtitles() logging.info('BAZARR All existing movie subtitles indexed from disk.') @@ -59,7 +64,7 @@ def update_movie(updated_movie, send_event): def get_movie_monitored_status(movie_id): existing_movie_monitored = database.execute( select(TableMovies.monitored) - .where(TableMovies.tmdbId == movie_id))\ + .where(TableMovies.tmdbId == str(movie_id)))\ .first() if existing_movie_monitored is None: return True @@ -108,6 +113,7 @@ def update_movies(send_event=True): else: audio_profiles = get_profile_list() tagsDict = get_tags() + language_profiles = get_language_profiles() # Get movies data from radarr movies = get_movies_from_radarr_api(apikey_radarr=apikey_radarr) @@ -178,6 +184,7 @@ def update_movies(send_event=True): if str(movie['tmdbId']) in current_movies_id_db: parsed_movie = movieParser(movie, action='update', tags_dict=tagsDict, + language_profiles=language_profiles, movie_default_profile=movie_default_profile, audio_profiles=audio_profiles) if not any([parsed_movie.items() <= x for x in current_movies_db_kv]): @@ -186,6 +193,7 @@ def update_movies(send_event=True): else: parsed_movie = movieParser(movie, action='insert', tags_dict=tagsDict, + language_profiles=language_profiles, movie_default_profile=movie_default_profile, audio_profiles=audio_profiles) add_movie(parsed_movie, send_event) @@ -247,6 +255,7 @@ def update_one_movie(movie_id, action, defer_search=False): audio_profiles = get_profile_list() tagsDict = get_tags() + language_profiles = get_language_profiles() try: # Get movie data from radarr api @@ -256,10 +265,10 @@ def update_one_movie(movie_id, action, defer_search=False): return else: if action == 'updated' and existing_movie: - movie = movieParser(movie_data, action='update', tags_dict=tagsDict, + movie = movieParser(movie_data, action='update', tags_dict=tagsDict, language_profiles=language_profiles, movie_default_profile=movie_default_profile, audio_profiles=audio_profiles) elif action == 'updated' and not existing_movie: - movie = movieParser(movie_data, action='insert', tags_dict=tagsDict, + movie = movieParser(movie_data, action='insert', tags_dict=tagsDict, language_profiles=language_profiles, movie_default_profile=movie_default_profile, audio_profiles=audio_profiles) except Exception: logging.exception('BAZARR cannot get movie returned by SignalR feed from Radarr API.') diff --git a/bazarr/radarr/sync/parser.py b/bazarr/radarr/sync/parser.py index 9648152c2..cc186df2f 100644 --- a/bazarr/radarr/sync/parser.py +++ b/bazarr/radarr/sync/parser.py @@ -3,7 +3,7 @@ import os from app.config import settings -from languages.get_languages import language_from_alpha2 +from languages.get_languages import audio_language_from_name from radarr.info import get_radarr_info from utilities.video_analyzer import embedded_audio_reader from utilities.path_mappings import path_mappings @@ -11,7 +11,17 @@ from utilities.path_mappings import path_mappings from .converter import RadarrFormatAudioCodec, RadarrFormatVideoCodec -def movieParser(movie, action, tags_dict, movie_default_profile, audio_profiles): +def get_matching_profile(tags, language_profiles): + matching_profile = None + if len(tags) > 0: + for profileId, name, tag in language_profiles: + if tag in tags: + matching_profile = profileId + break + return matching_profile + + +def movieParser(movie, action, tags_dict, language_profiles, movie_default_profile, audio_profiles): if 'movieFile' in movie: try: overview = str(movie['overview']) @@ -107,9 +117,7 @@ def movieParser(movie, action, tags_dict, movie_default_profile, audio_profiles) for item in movie['movieFile']['languages']: if isinstance(item, dict): if 'name' in item: - language = item['name'] - if item['name'] == 'Portuguese (Brazil)': - language = language_from_alpha2('pb') + language = audio_language_from_name(item['name']) audio_language.append(language) tags = [d['label'] for d in tags_dict if d['id'] in movie['tags']] @@ -140,6 +148,15 @@ def movieParser(movie, action, tags_dict, movie_default_profile, audio_profiles) parsed_movie['subtitles'] = '[]' parsed_movie['profileId'] = movie_default_profile + if settings.general.movie_tag_enabled: + tag_profile = get_matching_profile(tags, language_profiles) + if tag_profile: + parsed_movie['profileId'] = tag_profile + remove_profile_tags_list = settings.general.remove_profile_tags + if len(remove_profile_tags_list) > 0: + if set(tags) & set(remove_profile_tags_list): + parsed_movie['profileId'] = None + return parsed_movie diff --git a/bazarr/sonarr/sync/parser.py b/bazarr/sonarr/sync/parser.py index d8fce1697..27da32117 100644 --- a/bazarr/sonarr/sync/parser.py +++ b/bazarr/sonarr/sync/parser.py @@ -5,6 +5,7 @@ import os from app.config import settings from app.database import TableShows, database, select from constants import MINIMUM_VIDEO_SIZE +from languages.get_languages import audio_language_from_name from utilities.path_mappings import path_mappings from utilities.video_analyzer import embedded_audio_reader from sonarr.info import get_sonarr_info @@ -12,7 +13,17 @@ from sonarr.info import get_sonarr_info from .converter import SonarrFormatVideoCodec, SonarrFormatAudioCodec -def seriesParser(show, action, tags_dict, serie_default_profile, audio_profiles): +def get_matching_profile(tags, language_profiles): + matching_profile = None + if len(tags) > 0: + for profileId, name, tag in language_profiles: + if tag in tags: + matching_profile = profileId + break + return matching_profile + + +def seriesParser(show, action, tags_dict, language_profiles, serie_default_profile, audio_profiles): overview = show['overview'] if 'overview' in show else '' poster = '' fanart = '' @@ -24,9 +35,11 @@ def seriesParser(show, action, tags_dict, serie_default_profile, audio_profiles) if image['coverType'] == 'fanart': fanart = image['url'].split('?')[0] - alternate_titles = None if show['alternateTitles'] is not None: - alternate_titles = str([item['title'] for item in show['alternateTitles']]) + alternate_titles = [item['title'] for item in show['alternateTitles'] if 'title' in item and item['title'] not + in [None, ''] and item["title"] != show["title"]] + else: + alternate_titles = [] tags = [d['label'] for d in tags_dict if d['id'] in show['tags']] @@ -42,39 +55,37 @@ def seriesParser(show, action, tags_dict, serie_default_profile, audio_profiles) else: audio_language = [] - if action == 'update': - return {'title': show["title"], - 'path': show["path"], - 'tvdbId': int(show["tvdbId"]), - 'sonarrSeriesId': int(show["id"]), - 'overview': overview, - 'poster': poster, - 'fanart': fanart, - 'audio_language': str(audio_language), - 'sortTitle': show['sortTitle'], - 'year': str(show['year']), - 'alternativeTitles': alternate_titles, - 'tags': str(tags), - 'seriesType': show['seriesType'], - 'imdbId': imdbId, - 'monitored': str(bool(show['monitored']))} - else: - return {'title': show["title"], - 'path': show["path"], - 'tvdbId': show["tvdbId"], - 'sonarrSeriesId': show["id"], - 'overview': overview, - 'poster': poster, - 'fanart': fanart, - 'audio_language': str(audio_language), - 'sortTitle': show['sortTitle'], - 'year': str(show['year']), - 'alternativeTitles': alternate_titles, - 'tags': str(tags), - 'seriesType': show['seriesType'], - 'imdbId': imdbId, - 'profileId': serie_default_profile, - 'monitored': str(bool(show['monitored']))} + parsed_series = { + 'title': show["title"], + 'path': show["path"], + 'tvdbId': int(show["tvdbId"]), + 'sonarrSeriesId': int(show["id"]), + 'overview': overview, + 'poster': poster, + 'fanart': fanart, + 'audio_language': str(audio_language), + 'sortTitle': show['sortTitle'], + 'year': str(show['year']), + 'alternativeTitles': str(alternate_titles), + 'tags': str(tags), + 'seriesType': show['seriesType'], + 'imdbId': imdbId, + 'monitored': str(bool(show['monitored'])) + } + + if action == 'insert': + parsed_series['profileId'] = serie_default_profile + + if settings.general.serie_tag_enabled: + tag_profile = get_matching_profile(tags, language_profiles) + if tag_profile: + parsed_series['profileId'] = tag_profile + remove_profile_tags_list = settings.general.remove_profile_tags + if len(remove_profile_tags_list) > 0: + if set(tags) & set(remove_profile_tags_list): + parsed_series['profileId'] = None + + return parsed_series def profile_id_to_language(id_, profiles): @@ -111,13 +122,13 @@ def episodeParser(episode): item = episode['episodeFile']['language'] if isinstance(item, dict): if 'name' in item: - audio_language.append(item['name']) + audio_language.append(audio_language_from_name(item['name'])) elif 'languages' in episode['episodeFile'] and len(episode['episodeFile']['languages']): items = episode['episodeFile']['languages'] if isinstance(items, list): for item in items: if 'name' in item: - audio_language.append(item['name']) + audio_language.append(audio_language_from_name(item['name'])) else: audio_language = database.execute( select(TableShows.audio_language) diff --git a/bazarr/sonarr/sync/series.py b/bazarr/sonarr/sync/series.py index 96a00009c..f8bd84990 100644 --- a/bazarr/sonarr/sync/series.py +++ b/bazarr/sonarr/sync/series.py @@ -26,6 +26,11 @@ def trace(message): logging.debug(FEATURE_PREFIX + message) +def get_language_profiles(): + return database.execute( + select(TableLanguagesProfiles.profileId, TableLanguagesProfiles.name, TableLanguagesProfiles.tag)).all() + + def get_series_monitored_table(): series_monitored = database.execute( select(TableShows.tvdbId, TableShows.monitored))\ @@ -58,6 +63,7 @@ def update_series(send_event=True): audio_profiles = get_profile_list() tagsDict = get_tags() + language_profiles = get_language_profiles() # Get shows data from Sonarr series = get_series_from_sonarr_api(apikey_sonarr=apikey_sonarr) @@ -111,6 +117,7 @@ def update_series(send_event=True): if show['id'] in current_shows_db: updated_series = seriesParser(show, action='update', tags_dict=tagsDict, + language_profiles=language_profiles, serie_default_profile=serie_default_profile, audio_profiles=audio_profiles) @@ -132,6 +139,7 @@ def update_series(send_event=True): event_stream(type='series', payload=show['id']) else: added_series = seriesParser(show, action='insert', tags_dict=tagsDict, + language_profiles=language_profiles, serie_default_profile=serie_default_profile, audio_profiles=audio_profiles) @@ -203,7 +211,7 @@ def update_one_series(series_id, action): audio_profiles = get_profile_list() tagsDict = get_tags() - + language_profiles = get_language_profiles() try: # Get series data from sonarr api series = None @@ -215,10 +223,12 @@ def update_one_series(series_id, action): else: if action == 'updated' and existing_series: series = seriesParser(series_data[0], action='update', tags_dict=tagsDict, + language_profiles=language_profiles, serie_default_profile=serie_default_profile, audio_profiles=audio_profiles) elif action == 'updated' and not existing_series: series = seriesParser(series_data[0], action='insert', tags_dict=tagsDict, + language_profiles=language_profiles, serie_default_profile=serie_default_profile, audio_profiles=audio_profiles) except Exception: diff --git a/bazarr/subtitles/indexer/movies.py b/bazarr/subtitles/indexer/movies.py index 48c93001d..563a68cff 100644 --- a/bazarr/subtitles/indexer/movies.py +++ b/bazarr/subtitles/indexer/movies.py @@ -182,7 +182,9 @@ def list_missing_subtitles_movies(no=None, send_event=True): if any(x['code2'] == language['language'] for x in get_audio_profile_languages( movie_subtitles.audio_language)): continue - desired_subtitles_list.append([language['language'], language['forced'], language['hi']]) + desired_subtitles_list.append({'language': language['language'], + 'forced': language['forced'], + 'hi': language['hi']}) # get existing subtitles actual_subtitles_list = [] @@ -204,7 +206,9 @@ def list_missing_subtitles_movies(no=None, send_event=True): elif subtitles[1] == 'hi': forced = False hi = True - actual_subtitles_list.append([lang, str(forced), str(hi)]) + actual_subtitles_list.append({'language': lang, + 'forced': str(forced), + 'hi': str(hi)}) # check if cutoff is reached and skip any further check cutoff_met = False @@ -212,7 +216,9 @@ def list_missing_subtitles_movies(no=None, send_event=True): if cutoff_temp_list: for cutoff_temp in cutoff_temp_list: - cutoff_language = [cutoff_temp['language'], cutoff_temp['forced'], cutoff_temp['hi']] + cutoff_language = {'language': cutoff_temp['language'], + 'forced': cutoff_temp['forced'], + 'hi': cutoff_temp['hi']} if cutoff_temp['audio_exclude'] == 'True' and \ any(x['code2'] == cutoff_temp['language'] for x in get_audio_profile_languages(movie_subtitles.audio_language)): @@ -220,7 +226,10 @@ def list_missing_subtitles_movies(no=None, send_event=True): elif cutoff_language in actual_subtitles_list: cutoff_met = True # HI is considered as good as normal - elif cutoff_language and [cutoff_language[0], 'False', 'True'] in actual_subtitles_list: + elif (cutoff_language and + {'language': cutoff_language['language'], + 'forced': 'False', + 'hi': 'True'} in actual_subtitles_list): cutoff_met = True if cutoff_met: @@ -232,21 +241,23 @@ def list_missing_subtitles_movies(no=None, send_event=True): if item not in actual_subtitles_list: missing_subtitles_list.append(item) - # remove missing that have forced or hi subtitles for this language in existing + # remove missing that have forced or hi subtitles for this language in existing for item in actual_subtitles_list: - if item[2] == 'True': + if item['hi'] == 'True': try: - missing_subtitles_list.remove([item[0], 'False', 'False']) + missing_subtitles_list.remove({'language': item['language'], + 'forced': 'False', + 'hi': 'False'}) except ValueError: pass # make the missing languages list looks like expected missing_subtitles_output_list = [] for item in missing_subtitles_list: - lang = item[0] - if item[1] == 'True': + lang = item['language'] + if item['forced'] == 'True': lang += ':forced' - elif item[2] == 'True': + elif item['hi'] == 'True': lang += ':hi' missing_subtitles_output_list.append(lang) diff --git a/bazarr/subtitles/indexer/series.py b/bazarr/subtitles/indexer/series.py index 52622e9ea..64af2e4c3 100644 --- a/bazarr/subtitles/indexer/series.py +++ b/bazarr/subtitles/indexer/series.py @@ -182,7 +182,9 @@ def list_missing_subtitles(no=None, epno=None, send_event=True): if any(x['code2'] == language['language'] for x in get_audio_profile_languages( episode_subtitles.audio_language)): continue - desired_subtitles_list.append([language['language'], language['forced'], language['hi']]) + desired_subtitles_list.append({'language': language['language'], + 'forced': language['forced'], + 'hi': language['hi']}) # get existing subtitles actual_subtitles_list = [] @@ -204,7 +206,9 @@ def list_missing_subtitles(no=None, epno=None, send_event=True): elif subtitles[1] == 'hi': forced = False hi = True - actual_subtitles_list.append([lang, str(forced), str(hi)]) + actual_subtitles_list.append({'language': lang, + 'forced': str(forced), + 'hi': str(hi)}) # check if cutoff is reached and skip any further check cutoff_met = False @@ -212,7 +216,9 @@ def list_missing_subtitles(no=None, epno=None, send_event=True): if cutoff_temp_list: for cutoff_temp in cutoff_temp_list: - cutoff_language = [cutoff_temp['language'], cutoff_temp['forced'], cutoff_temp['hi']] + cutoff_language = {'language': cutoff_temp['language'], + 'forced': cutoff_temp['forced'], + 'hi': cutoff_temp['hi']} if cutoff_temp['audio_exclude'] == 'True' and \ any(x['code2'] == cutoff_temp['language'] for x in get_audio_profile_languages(episode_subtitles.audio_language)): @@ -220,7 +226,10 @@ def list_missing_subtitles(no=None, epno=None, send_event=True): elif cutoff_language in actual_subtitles_list: cutoff_met = True # HI is considered as good as normal - elif [cutoff_language[0], 'False', 'True'] in actual_subtitles_list: + elif (cutoff_language and + {'language': cutoff_language['language'], + 'forced': 'False', + 'hi': 'True'} in actual_subtitles_list): cutoff_met = True if cutoff_met: @@ -234,21 +243,23 @@ def list_missing_subtitles(no=None, epno=None, send_event=True): if item not in actual_subtitles_list: missing_subtitles_list.append(item) - # remove missing that have hi subtitles for this language in existing + # remove missing that have hi subtitles for this language in existing for item in actual_subtitles_list: - if item[2] == 'True': + if item['hi'] == 'True': try: - missing_subtitles_list.remove([item[0], 'False', 'False']) + missing_subtitles_list.remove({'language': item['language'], + 'forced': 'False', + 'hi': 'False'}) except ValueError: pass # make the missing languages list looks like expected missing_subtitles_output_list = [] for item in missing_subtitles_list: - lang = item[0] - if item[1] == 'True': + lang = item['language'] + if item['forced'] == 'True': lang += ':forced' - elif item[2] == 'True': + elif item['hi'] == 'True': lang += ':hi' missing_subtitles_output_list.append(lang) diff --git a/bazarr/subtitles/indexer/utils.py b/bazarr/subtitles/indexer/utils.py index 03549d748..63b9dee6d 100644 --- a/bazarr/subtitles/indexer/utils.py +++ b/bazarr/subtitles/indexer/utils.py @@ -2,7 +2,6 @@ import os import logging -import re from guess_language import guess_language from subliminal_patch import core @@ -136,6 +135,7 @@ def guess_external_subtitles(dest_folder, subtitles, media_type, previously_inde continue text = text.decode(encoding) - if bool(re.search(core.HI_REGEX, text)): + if core.parse_for_hi_regex(subtitle_text=text, + alpha3_language=language.alpha3 if hasattr(language, 'alpha3') else None): subtitles[subtitle] = Language.rebuild(subtitles[subtitle], forced=False, hi=True) return subtitles diff --git a/bazarr/subtitles/manual.py b/bazarr/subtitles/manual.py index ba57eb193..5d642b577 100644 --- a/bazarr/subtitles/manual.py +++ b/bazarr/subtitles/manual.py @@ -158,8 +158,9 @@ def manual_download_subtitle(path, audio_language, hi, forced, subtitle, provide subtitle.language.forced = True else: subtitle.language.forced = False - if use_original_format == 'True': - subtitle.use_original_format = use_original_format + if use_original_format in ("1", "True"): + subtitle.use_original_format = True + subtitle.mods = get_array_from(settings.general.subzero_mods) video = get_video(force_unicode(path), title, sceneName, providers={provider}, media_type=media_type) if video: diff --git a/bazarr/subtitles/processing.py b/bazarr/subtitles/processing.py index b5c032610..c008e72fa 100644 --- a/bazarr/subtitles/processing.py +++ b/bazarr/subtitles/processing.py @@ -88,6 +88,7 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u from .sync import sync_subtitles sync_subtitles(video_path=path, srt_path=downloaded_path, forced=subtitle.language.forced, + hi=subtitle.language.hi, srt_lang=downloaded_language_code2, percent_score=percent_score, sonarr_series_id=episode_metadata.sonarrSeriesId, @@ -106,6 +107,7 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u from .sync import sync_subtitles sync_subtitles(video_path=path, srt_path=downloaded_path, forced=subtitle.language.forced, + hi=subtitle.language.hi, srt_lang=downloaded_language_code2, percent_score=percent_score, radarr_id=movie_metadata.radarrId) diff --git a/bazarr/subtitles/refiners/__init__.py b/bazarr/subtitles/refiners/__init__.py index ff1e715a0..9fbdecbb2 100644 --- a/bazarr/subtitles/refiners/__init__.py +++ b/bazarr/subtitles/refiners/__init__.py @@ -4,10 +4,12 @@ from .ffprobe import refine_from_ffprobe from .database import refine_from_db from .arr_history import refine_from_arr_history from .anidb import refine_from_anidb +from .anilist import refine_from_anilist registered = { "database": refine_from_db, "ffprobe": refine_from_ffprobe, "arr_history": refine_from_arr_history, "anidb": refine_from_anidb, + "anilist": refine_from_anilist, # Must run AFTER AniDB } diff --git a/bazarr/subtitles/refiners/anidb.py b/bazarr/subtitles/refiners/anidb.py index 88ee55ecb..f896697bd 100644 --- a/bazarr/subtitles/refiners/anidb.py +++ b/bazarr/subtitles/refiners/anidb.py @@ -4,11 +4,13 @@ import logging import requests from collections import namedtuple -from datetime import timedelta +from datetime import datetime, timedelta from requests.exceptions import HTTPError from app.config import settings from subliminal import Episode, region +from subliminal.cache import REFINER_EXPIRATION_TIME +from subliminal_patch.exceptions import TooManyRequests try: from lxml import etree @@ -18,16 +20,40 @@ except ImportError: except ImportError: import xml.etree.ElementTree as etree -refined_providers = {'animetosho'} +refined_providers = {'animetosho', 'jimaku'} +providers_requiring_anidb_api = {'animetosho'} + +logger = logging.getLogger(__name__) api_url = 'http://api.anidb.net:9001/httpapi' +cache_key_refiner = "anidb_refiner" + +# Soft Limit for amount of requests per day +daily_limit_request_count = 200 + class AniDBClient(object): def __init__(self, api_client_key=None, api_client_ver=1, session=None): self.session = session or requests.Session() self.api_client_key = api_client_key self.api_client_ver = api_client_ver + self.cache = region.get(cache_key_refiner, expiration_time=timedelta(days=1).total_seconds()) + + @property + def is_throttled(self): + return self.cache and self.cache.get('is_throttled') + + @property + def has_api_credentials(self): + return self.api_client_key != '' and self.api_client_key is not None + + @property + def daily_api_request_count(self): + if not self.cache: + return 0 + + return self.cache.get('daily_api_request_count', 0) AnimeInfo = namedtuple('AnimeInfo', ['anime', 'episode_offset']) @@ -43,7 +69,9 @@ class AniDBClient(object): return r.content @region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds()) - def get_series_id(self, mappings, tvdb_series_season, tvdb_series_id, episode): + def get_show_information(self, tvdb_series_id, tvdb_series_season, episode): + mappings = etree.fromstring(self.get_series_mappings()) + # Enrich the collection of anime with the episode offset animes = [ self.AnimeInfo(anime, int(anime.attrib.get('episodeoffset', 0))) @@ -52,40 +80,78 @@ class AniDBClient(object): ) ] + is_special_entry = False if not animes: - return None, None + # Some entries will store TVDB seasons in a nested mapping list, identifiable by the value 'a' as the season + special_entries = mappings.findall( + f".//anime[@tvdbid='{tvdb_series_id}'][@defaulttvdbseason='a']" + ) - # Sort the anime by offset in ascending order - animes.sort(key=lambda a: a.episode_offset) + if not special_entries: + return None, None, None - # Different from Tvdb, Anidb have different ids for the Parts of a season - anidb_id = None - offset = 0 + is_special_entry = True + for special_entry in special_entries: + mapping_list = special_entry.findall(f".//mapping[@tvdbseason='{tvdb_series_season}']") + if len(mapping_list) > 0: + anidb_id = int(special_entry.attrib.get('anidbid')) + offset = int(mapping_list[0].attrib.get('offset', 0)) - for index, anime_info in enumerate(animes): - anime, episode_offset = anime_info - anidb_id = int(anime.attrib.get('anidbid')) - if episode > episode_offset: - anidb_id = anidb_id - offset = episode_offset + if not is_special_entry: + # Sort the anime by offset in ascending order + animes.sort(key=lambda a: a.episode_offset) - return anidb_id, episode - offset + # Different from Tvdb, Anidb have different ids for the Parts of a season + anidb_id = None + offset = 0 - @region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds()) - def get_series_episodes_ids(self, tvdb_series_id, season, episode): - mappings = etree.fromstring(self.get_series_mappings()) + for index, anime_info in enumerate(animes): + anime, episode_offset = anime_info + + mapping_list = anime.find('mapping-list') + + # Handle mapping list for Specials + if mapping_list: + for mapping in mapping_list.findall("mapping"): + if mapping.text is None: + continue + + # Mapping values are usually like ;1-1;2-1;3-1; + for episode_ref in mapping.text.split(';'): + if not episode_ref: + continue + + anidb_episode, tvdb_episode = map(int, episode_ref.split('-')) + if tvdb_episode == episode: + anidb_id = int(anime.attrib.get('anidbid')) + + return anidb_id, anidb_episode, 0 + + if episode > episode_offset: + anidb_id = int(anime.attrib.get('anidbid')) + offset = episode_offset - series_id, episode_no = self.get_series_id(mappings, season, tvdb_series_id, episode) + return anidb_id, episode - offset, offset + @region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds()) + def get_episode_ids(self, series_id, episode_no): if not series_id: - return None, None + return None episodes = etree.fromstring(self.get_episodes(series_id)) - return series_id, int(episodes.find(f".//episode[epno='{episode_no}']").attrib.get('id')) + episode = episodes.find(f".//episode[epno='{episode_no}']") - @region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds()) + if not episode: + return series_id, None + + return series_id, int(episode.attrib.get('id')) + + @region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME) def get_episodes(self, series_id): + if self.daily_api_request_count >= 200: + raise TooManyRequests('Daily API request limit exceeded') + r = self.session.get( api_url, params={ @@ -102,10 +168,12 @@ class AniDBClient(object): response_code = xml_root.attrib.get('code') if response_code == '500': - raise HTTPError('AniDB API Abuse detected. Banned status.') + raise TooManyRequests('AniDB API Abuse detected. Banned status.') elif response_code == '302': raise HTTPError('AniDB API Client error. Client is disabled or does not exists.') + self.increment_daily_quota() + episode_elements = xml_root.find('episodes') if not episode_elements: @@ -113,11 +181,25 @@ class AniDBClient(object): return etree.tostring(episode_elements, encoding='utf8', method='xml') + def increment_daily_quota(self): + daily_quota = self.daily_api_request_count + 1 + + if not self.cache: + region.set(cache_key_refiner, {'daily_api_request_count': daily_quota}) + + return + + self.cache['daily_api_request_count'] = daily_quota + + region.set(cache_key_refiner, self.cache) + + @staticmethod + def mark_as_throttled(): + region.set(cache_key_refiner, {'is_throttled': True}) + def refine_from_anidb(path, video): if not isinstance(video, Episode) or not video.series_tvdb_id: - logging.debug(f'Video is not an Anime TV series, skipping refinement for {video}') - return if refined_providers.intersection(settings.general.enabled_providers) and video.series_anidb_id is None: @@ -129,12 +211,35 @@ def refine_anidb_ids(video): season = video.season if video.season else 0 - anidb_series_id, anidb_episode_id = anidb_client.get_series_episodes_ids(video.series_tvdb_id, season, video.episode) - - if not anidb_episode_id: - logging.error(f'Could not find anime series {video.series}') - + anidb_series_id, anidb_episode_no, anidb_season_episode_offset = anidb_client.get_show_information( + video.series_tvdb_id, + season, + video.episode, + ) + + if not anidb_series_id: + logger.error(f'Could not find anime series {video.series}') return video + + anidb_episode_id = None + if anidb_client.has_api_credentials: + if anidb_client.is_throttled: + logger.warning(f'API daily limit reached. Skipping episode ID refinement for {video.series}') + else: + try: + anidb_episode_id = anidb_client.get_episode_ids( + anidb_series_id, + anidb_episode_no + ) + except TooManyRequests: + logger.error(f'API daily limit reached while refining {video.series}') + anidb_client.mark_as_throttled() + else: + intersect = providers_requiring_anidb_api.intersection(settings.general.enabled_providers) + if len(intersect) >= 1: + logger.warn(f'AniDB API credentials are not fully set up, the following providers may not work: {intersect}') video.series_anidb_id = anidb_series_id video.series_anidb_episode_id = anidb_episode_id + video.series_anidb_episode_no = anidb_episode_no + video.series_anidb_season_episode_offset = anidb_season_episode_offset diff --git a/bazarr/subtitles/refiners/anilist.py b/bazarr/subtitles/refiners/anilist.py new file mode 100644 index 000000000..4a008a5e1 --- /dev/null +++ b/bazarr/subtitles/refiners/anilist.py @@ -0,0 +1,79 @@ +# coding=utf-8 +# fmt: off + +import logging +import time +import requests +from collections import namedtuple +from datetime import timedelta + +from app.config import settings +from subliminal import Episode, region, __short_version__ + +logger = logging.getLogger(__name__) +refined_providers = {'jimaku'} + + +class AniListClient(object): + def __init__(self, session=None, timeout=10): + self.session = session or requests.Session() + self.session.timeout = timeout + self.session.headers['Content-Type'] = 'application/json' + self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__ + + @region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds()) + def get_series_mappings(self): + r = self.session.get( + 'https://raw.githubusercontent.com/Fribb/anime-lists/master/anime-list-mini.json' + ) + + r.raise_for_status() + return r.json() + + def get_series_id(self, candidate_id_name, candidate_id_value): + anime_list = self.get_series_mappings() + + tag_map = { + "series_anidb_id": "anidb_id", + "imdb_id": "imdb_id" + } + mapped_tag = tag_map.get(candidate_id_name, candidate_id_name) + + obj = [obj for obj in anime_list if mapped_tag in obj and str(obj[mapped_tag]) == str(candidate_id_value)] + logger.debug(f"Based on '{mapped_tag}': '{candidate_id_value}', anime-list matched: {obj}") + + if len(obj) > 0: + return obj[0]["anilist_id"] + else: + logger.debug(f"Could not find corresponding AniList ID with '{mapped_tag}': {candidate_id_value}") + return None + + +def refine_from_anilist(path, video): + # Safety checks + if isinstance(video, Episode): + if not video.series_anidb_id: + return + + if refined_providers.intersection(settings.general.enabled_providers) and video.anilist_id is None: + refine_anilist_ids(video) + + +def refine_anilist_ids(video): + anilist_client = AniListClient() + + if isinstance(video, Episode): + candidate_id_name = "series_anidb_id" + else: + candidate_id_name = "imdb_id" + + candidate_id_value = getattr(video, candidate_id_name, None) + if not candidate_id_value: + logger.error(f"Found no value for property {candidate_id_name} of video.") + return video + + anilist_id = anilist_client.get_series_id(candidate_id_name, candidate_id_value) + if not anilist_id: + return video + + video.anilist_id = anilist_id diff --git a/bazarr/subtitles/sync.py b/bazarr/subtitles/sync.py index f7a3a69c4..4726d245f 100644 --- a/bazarr/subtitles/sync.py +++ b/bazarr/subtitles/sync.py @@ -8,7 +8,7 @@ from app.config import settings from subtitles.tools.subsyncer import SubSyncer -def sync_subtitles(video_path, srt_path, srt_lang, forced, percent_score, sonarr_series_id=None, +def sync_subtitles(video_path, srt_path, srt_lang, forced, hi, percent_score, sonarr_series_id=None, sonarr_episode_id=None, radarr_id=None): if forced: logging.debug('BAZARR cannot sync forced subtitles. Skipping sync routine.') @@ -30,6 +30,8 @@ def sync_subtitles(video_path, srt_path, srt_lang, forced, percent_score, sonarr 'video_path': video_path, 'srt_path': srt_path, 'srt_lang': srt_lang, + 'forced': forced, + 'hi': hi, 'max_offset_seconds': str(settings.subsync.max_offset_seconds), 'no_fix_framerate': settings.subsync.no_fix_framerate, 'gss': settings.subsync.gss, diff --git a/bazarr/subtitles/tools/subsyncer.py b/bazarr/subtitles/tools/subsyncer.py index a71424516..5c7ae38b0 100644 --- a/bazarr/subtitles/tools/subsyncer.py +++ b/bazarr/subtitles/tools/subsyncer.py @@ -30,7 +30,7 @@ class SubSyncer: self.vad = 'subs_then_webrtc' self.log_dir_path = os.path.join(args.config_dir, 'log') - def sync(self, video_path, srt_path, srt_lang, + def sync(self, video_path, srt_path, srt_lang, hi, forced, max_offset_seconds, no_fix_framerate, gss, reference=None, sonarr_series_id=None, sonarr_episode_id=None, radarr_id=None): self.reference = video_path @@ -97,8 +97,7 @@ class SubSyncer: result = run(self.args) except Exception: logging.exception( - f'BAZARR an exception occurs during the synchronization process for this subtitles: {self.srtin}') - raise OSError + f'BAZARR an exception occurs during the synchronization process for this subtitle file: {self.srtin}') else: if settings.subsync.debug: return result @@ -118,10 +117,10 @@ class SubSyncer: downloaded_language_code2=srt_lang, downloaded_provider=None, score=None, - forced=None, + forced=forced, subtitle_id=None, reversed_subtitles_path=srt_path, - hearing_impaired=None) + hearing_impaired=hi) if sonarr_episode_id: history_log(action=5, sonarr_series_id=sonarr_series_id, sonarr_episode_id=sonarr_episode_id, diff --git a/bazarr/subtitles/tools/translate.py b/bazarr/subtitles/tools/translate.py index d91c6a4ba..534200e20 100644 --- a/bazarr/subtitles/tools/translate.py +++ b/bazarr/subtitles/tools/translate.py @@ -6,12 +6,16 @@ import pysubs2 from subliminal_patch.core import get_subtitle_path from subzero.language import Language from deep_translator import GoogleTranslator +from deep_translator.exceptions import TooManyRequests, RequestError, TranslationNotFound +from time import sleep +from concurrent.futures import ThreadPoolExecutor from languages.custom_lang import CustomLanguage from languages.get_languages import alpha3_from_alpha2, language_from_alpha2, language_from_alpha3 from radarr.history import history_log_movie from sonarr.history import history_log from subtitles.processing import ProcessSubtitlesResult +from app.event_handler import show_progress, hide_progress def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, forced, hi, media_type, sonarr_series_id, @@ -33,8 +37,6 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo logging.debug(f'BAZARR is translating in {lang_obj} this subtitles {source_srt_file}') - max_characters = 5000 - dest_srt_file = get_subtitle_path(video_path, language=lang_obj if isinstance(lang_obj, Language) else lang_obj.subzero_language(), extension='.srt', @@ -44,40 +46,53 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo subs = pysubs2.load(source_srt_file, encoding='utf-8') subs.remove_miscellaneous_events() lines_list = [x.plaintext for x in subs] - joined_lines_str = '\n\n\n'.join(lines_list) - - logging.debug(f'BAZARR splitting subtitles into {max_characters} characters blocks') - lines_block_list = [] - translated_lines_list = [] - while len(joined_lines_str): - partial_lines_str = joined_lines_str[:max_characters] + lines_list_len = len(lines_list) - if len(joined_lines_str) > max_characters: - new_partial_lines_str = partial_lines_str.rsplit('\n\n\n', 1)[0] + def translate_line(id, line, attempt): + try: + translated_text = GoogleTranslator( + source='auto', + target=language_code_convert_dict.get(lang_obj.alpha2, lang_obj.alpha2) + ).translate(text=line) + except TooManyRequests: + if attempt <= 5: + sleep(1) + super(translate_line(id, line, attempt+1)) + else: + logging.debug(f'Too many requests while translating {line}') + translated_lines.append({'id': id, 'line': line}) + except (RequestError, TranslationNotFound): + logging.debug(f'Unable to translate line {line}') + translated_lines.append({'id': id, 'line': line}) else: - new_partial_lines_str = partial_lines_str + translated_lines.append({'id': id, 'line': translated_text}) + finally: + show_progress(id=f'translate_progress_{dest_srt_file}', + header=f'Translating subtitles lines to {language_from_alpha3(to_lang)}...', + name='', + value=len(translated_lines), + count=lines_list_len) - lines_block_list.append(new_partial_lines_str) - joined_lines_str = joined_lines_str.replace(new_partial_lines_str, '') + logging.debug(f'BAZARR is sending {lines_list_len} blocks to Google Translate') - logging.debug(f'BAZARR is sending {len(lines_block_list)} blocks to Google Translate') - for block_str in lines_block_list: - try: - translated_partial_srt_text = GoogleTranslator(source='auto', - target=language_code_convert_dict.get(lang_obj.alpha2, - lang_obj.alpha2) - ).translate(text=block_str) - except Exception: - logging.exception(f'BAZARR Unable to translate subtitles {source_srt_file}') - return False - else: - translated_partial_srt_list = translated_partial_srt_text.split('\n\n\n') - translated_lines_list += translated_partial_srt_list + pool = ThreadPoolExecutor(max_workers=10) + + translated_lines = [] + + for i, line in enumerate(lines_list): + pool.submit(translate_line, i, line, 1) + + pool.shutdown(wait=True) + + for i, line in enumerate(translated_lines): + lines_list[line['id']] = line['line'] + + hide_progress(id=f'translate_progress_{dest_srt_file}') logging.debug(f'BAZARR saving translated subtitles to {dest_srt_file}') for i, line in enumerate(subs): try: - line.plaintext = translated_lines_list[i] + line.plaintext = lines_list[i] except IndexError: logging.error(f'BAZARR is unable to translate malformed subtitles: {source_srt_file}') return False @@ -94,10 +109,10 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo downloaded_language_code2=to_lang, downloaded_provider=None, score=None, - forced=None, + forced=forced, subtitle_id=None, reversed_subtitles_path=dest_srt_file, - hearing_impaired=None) + hearing_impaired=hi) if media_type == 'series': history_log(action=6, sonarr_series_id=sonarr_series_id, sonarr_episode_id=sonarr_episode_id, result=result) diff --git a/bazarr/subtitles/upgrade.py b/bazarr/subtitles/upgrade.py index 4593c71bc..1c565bd0d 100644 --- a/bazarr/subtitles/upgrade.py +++ b/bazarr/subtitles/upgrade.py @@ -69,14 +69,12 @@ def upgrade_subtitles(): .join(TableEpisodes, onclause=TableHistory.sonarrEpisodeId == TableEpisodes.sonarrEpisodeId) .join(episodes_to_upgrade, onclause=TableHistory.id == episodes_to_upgrade.c.id, isouter=True) .where(episodes_to_upgrade.c.id.is_not(None))) - .all() if _language_still_desired(x.language, x.profileId)] + .all() if _language_still_desired(x.language, x.profileId) and + x.subtitles_path in x.external_subtitles and + x.video_path == x.path + ] for item in episodes_data: - if item['upgradable']: - if item['subtitles_path'] not in item['external_subtitles'] or \ - not item['video_path'] == item['path']: - item.update({"upgradable": False}) - del item['path'] del item['external_subtitles'] @@ -156,14 +154,12 @@ def upgrade_subtitles(): .join(TableMovies, onclause=TableHistoryMovie.radarrId == TableMovies.radarrId) .join(movies_to_upgrade, onclause=TableHistoryMovie.id == movies_to_upgrade.c.id, isouter=True) .where(movies_to_upgrade.c.id.is_not(None))) - .all() if _language_still_desired(x.language, x.profileId)] + .all() if _language_still_desired(x.language, x.profileId) and + x.subtitles_path in x.external_subtitles and + x.video_path == x.path + ] for item in movies_data: - if item['upgradable']: - if item['subtitles_path'] not in item['external_subtitles'] or \ - not item['video_path'] == item['path']: - item.update({"upgradable": False}) - del item['path'] del item['external_subtitles'] diff --git a/bazarr/subtitles/upload.py b/bazarr/subtitles/upload.py index 8ad16128e..cd5aebca5 100644 --- a/bazarr/subtitles/upload.py +++ b/bazarr/subtitles/upload.py @@ -138,7 +138,7 @@ def manual_upload_subtitle(path, language, forced, hi, media_type, subtitle, aud series_id = episode_metadata.sonarrSeriesId episode_id = episode_metadata.sonarrEpisodeId sync_subtitles(video_path=path, srt_path=subtitle_path, srt_lang=uploaded_language_code2, percent_score=100, - sonarr_series_id=episode_metadata.sonarrSeriesId, forced=forced, + sonarr_series_id=episode_metadata.sonarrSeriesId, forced=forced, hi=hi, sonarr_episode_id=episode_metadata.sonarrEpisodeId) else: if not movie_metadata: @@ -146,7 +146,7 @@ def manual_upload_subtitle(path, language, forced, hi, media_type, subtitle, aud series_id = "" episode_id = movie_metadata.radarrId sync_subtitles(video_path=path, srt_path=subtitle_path, srt_lang=uploaded_language_code2, percent_score=100, - radarr_id=movie_metadata.radarrId, forced=forced) + radarr_id=movie_metadata.radarrId, forced=forced, hi=hi) if use_postprocessing: command = pp_replace(postprocessing_cmd, path, subtitle_path, uploaded_language, uploaded_language_code2, diff --git a/bazarr/utilities/video_analyzer.py b/bazarr/utilities/video_analyzer.py index ff9467a94..dc9afacee 100644 --- a/bazarr/utilities/video_analyzer.py +++ b/bazarr/utilities/video_analyzer.py @@ -121,7 +121,9 @@ def subtitles_sync_references(subtitles_path, sonarr_episode_id=None, radarr_mov if not media_data: return references_dict - data = parse_video_metadata(media_data.path, media_data.file_size, media_data.episode_file_id, None, + mapped_path = path_mappings.path_replace(media_data.path) + + data = parse_video_metadata(mapped_path, media_data.file_size, media_data.episode_file_id, None, use_cache=True) elif radarr_movie_id: media_data = database.execute( @@ -132,7 +134,9 @@ def subtitles_sync_references(subtitles_path, sonarr_episode_id=None, radarr_mov if not media_data: return references_dict - data = parse_video_metadata(media_data.path, media_data.file_size, None, media_data.movie_file_id, + mapped_path = path_mappings.path_replace_movie(media_data.path) + + data = parse_video_metadata(mapped_path, media_data.file_size, None, media_data.movie_file_id, use_cache=True) if not data: @@ -213,6 +217,25 @@ def subtitles_sync_references(subtitles_path, sonarr_episode_id=None, radarr_mov def parse_video_metadata(file, file_size, episode_file_id=None, movie_file_id=None, use_cache=True): + """ + This function return the video file properties as parsed by knowit using ffprobe or mediainfo using the cached + value by default. + + @type file: string + @param file: Properly mapped path of a video file + @type file_size: int + @param file_size: File size in bytes of the video file + @type episode_file_id: int or None + @param episode_file_id: episode ID of the video file from Sonarr (or None if it's a movie) + @type movie_file_id: int or None + @param movie_file_id: movie ID of the video file from Radarr (or None if it's an episode) + @type use_cache: bool + @param use_cache: + + @rtype: dict or None + @return: return a dictionary including the video file properties as parsed by ffprobe or mediainfo + """ + # Define default data keys value data = { "ffprobe": {}, @@ -228,12 +251,12 @@ def parse_video_metadata(file, file_size, episode_file_id=None, movie_file_id=No if episode_file_id: cache_key = database.execute( select(TableEpisodes.ffprobe_cache) - .where(TableEpisodes.path == path_mappings.path_replace_reverse(file))) \ + .where(TableEpisodes.episode_file_id == episode_file_id)) \ .first() elif movie_file_id: cache_key = database.execute( select(TableMovies.ffprobe_cache) - .where(TableMovies.path == path_mappings.path_replace_reverse_movie(file))) \ + .where(TableMovies.movie_file_id == movie_file_id)) \ .first() else: cache_key = None @@ -243,6 +266,7 @@ def parse_video_metadata(file, file_size, episode_file_id=None, movie_file_id=No # Unpickle ffprobe cache cached_value = pickle.loads(cache_key.ffprobe_cache) except Exception: + # No cached value available, we'll parse the file pass else: # Check if file size and file id matches and if so, we return the cached value if available for the @@ -281,9 +305,7 @@ def parse_video_metadata(file, file_size, episode_file_id=None, movie_file_id=No # or if we have mediainfo available elif mediainfo_path: try: - # disabling mediainfo path temporarily until issue with knowit is fixed. - # data["mediainfo"] = know(video_path=file, context={"provider": "mediainfo", "mediainfo": mediainfo_path}) - data["mediainfo"] = know(video_path=file, context={"provider": "mediainfo"}) + data["mediainfo"] = know(video_path=file, context={"provider": "mediainfo", "mediainfo": mediainfo_path}) except KnowitException as e: logging.error(f"BAZARR mediainfo cannot analyze this video file {file}. Could it be corrupted? {e}") return None @@ -291,19 +313,19 @@ def parse_video_metadata(file, file_size, episode_file_id=None, movie_file_id=No else: logging.error("BAZARR require ffmpeg/ffprobe or mediainfo, please install it and make sure to choose it in " "Settings-->Subtitles.") - return + return None # we write to db the result and return the newly cached ffprobe dict if episode_file_id: database.execute( update(TableEpisodes) .values(ffprobe_cache=pickle.dumps(data, pickle.HIGHEST_PROTOCOL)) - .where(TableEpisodes.path == path_mappings.path_replace_reverse(file))) + .where(TableEpisodes.episode_file_id == episode_file_id)) elif movie_file_id: database.execute( update(TableMovies) .values(ffprobe_cache=pickle.dumps(data, pickle.HIGHEST_PROTOCOL)) - .where(TableMovies.path == path_mappings.path_replace_reverse_movie(file))) + .where(TableMovies.movie_file_id == movie_file_id)) return data diff --git a/custom_libs/subliminal/video.py b/custom_libs/subliminal/video.py index 2168d91a9..66c090945 100644 --- a/custom_libs/subliminal/video.py +++ b/custom_libs/subliminal/video.py @@ -130,7 +130,8 @@ class Episode(Video): """ def __init__(self, name, series, season, episode, title=None, year=None, original_series=True, tvdb_id=None, series_tvdb_id=None, series_imdb_id=None, alternative_series=None, series_anidb_id=None, - series_anidb_episode_id=None, **kwargs): + series_anidb_episode_id=None, series_anidb_season_episode_offset=None, + anilist_id=None, **kwargs): super(Episode, self).__init__(name, **kwargs) #: Series of the episode @@ -163,8 +164,11 @@ class Episode(Video): #: Alternative names of the series self.alternative_series = alternative_series or [] + #: Anime specific information self.series_anidb_episode_id = series_anidb_episode_id self.series_anidb_id = series_anidb_id + self.series_anidb_season_episode_offset = series_anidb_season_episode_offset + self.anilist_id = anilist_id @classmethod def fromguess(cls, name, guess): @@ -207,10 +211,11 @@ class Movie(Video): :param str title: title of the movie. :param int year: year of the movie. :param list alternative_titles: alternative titles of the movie + :param int anilist_id: AniList ID of movie (if Anime) :param \*\*kwargs: additional parameters for the :class:`Video` constructor. """ - def __init__(self, name, title, year=None, alternative_titles=None, **kwargs): + def __init__(self, name, title, year=None, alternative_titles=None, anilist_id=None, **kwargs): super(Movie, self).__init__(name, **kwargs) #: Title of the movie @@ -221,6 +226,9 @@ class Movie(Video): #: Alternative titles of the movie self.alternative_titles = alternative_titles or [] + + #: AniList ID of the movie + self.anilist_id = anilist_id @classmethod def fromguess(cls, name, guess): diff --git a/custom_libs/subliminal_patch/converters/subdl.py b/custom_libs/subliminal_patch/converters/subdl.py new file mode 100644 index 000000000..5381fc475 --- /dev/null +++ b/custom_libs/subliminal_patch/converters/subdl.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from babelfish import LanguageReverseConverter +from subliminal.exceptions import ConfigurationError + + +class SubdlConverter(LanguageReverseConverter): + def __init__(self): + self.from_subdl = { + "AR": ("ara", None, None), # Arabic + "DA": ("dan", None, None), # Danish + "NL": ("nld", None, None), # Dutch + "EN": ("eng", None, None), # English + "FA": ("fas", None, None), # Farsi_Persian + "FI": ("fin", None, None), # Finnish + "FR": ("fra", None, None), # French + "ID": ("ind", None, None), # Indonesian + "IT": ("ita", None, None), # Italian + "NO": ("nor", None, None), # Norwegian + "RO": ("ron", None, None), # Romanian + "ES": ("spa", None, None), # Spanish + "SV": ("swe", None, None), # Swedish + "VI": ("vie", None, None), # Vietnamese + "SQ": ("sqi", None, None), # Albanian + "AZ": ("aze", None, None), # Azerbaijani + "BE": ("bel", None, None), # Belarusian + "BN": ("ben", None, None), # Bengali + "BS": ("bos", None, None), # Bosnian + "BG": ("bul", None, None), # Bulgarian + "MY": ("mya", None, None), # Burmese + "CA": ("cat", None, None), # Catalan + "ZH": ("zho", None, None), # Chinese BG code + "HR": ("hrv", None, None), # Croatian + "CS": ("ces", None, None), # Czech + "EO": ("epo", None, None), # Esperanto + "ET": ("est", None, None), # Estonian + "KA": ("kat", None, None), # Georgian + "DE": ("deu", None, None), # German + "EL": ("ell", None, None), # Greek + "KL": ("kal", None, None), # Greenlandic + "HE": ("heb", None, None), # Hebrew + "HI": ("hin", None, None), # Hindi + "HU": ("hun", None, None), # Hungarian + "IS": ("isl", None, None), # Icelandic + "JA": ("jpn", None, None), # Japanese + "KO": ("kor", None, None), # Korean + "KU": ("kur", None, None), # Kurdish + "LV": ("lav", None, None), # Latvian + "LT": ("lit", None, None), # Lithuanian + "MK": ("mkd", None, None), # Macedonian + "MS": ("msa", None, None), # Malay + "ML": ("mal", None, None), # Malayalam + "PL": ("pol", None, None), # Polish + "PT": ("por", None, None), # Portuguese + "RU": ("rus", None, None), # Russian + "SR": ("srp", None, None), # Serbian + "SI": ("sin", None, None), # Sinhala + "SK": ("slk", None, None), # Slovak + "SL": ("slv", None, None), # Slovenian + "TL": ("tgl", None, None), # Tagalog + "TA": ("tam", None, None), # Tamil + "TE": ("tel", None, None), # Telugu + "TH": ("tha", None, None), # Thai + "TR": ("tur", None, None), # Turkish + "UK": ("ukr", None, None), # Ukrainian + "UR": ("urd", None, None), # Urdu + # custom languages + "BR_PT": ("por", "BR", None), # Brazilian Portuguese + "ZH_BG": ("zho", None, "Hant"), # Big 5 code + # unsupported language in Bazarr + # "BG_EN": "Bulgarian_English", + # "NL_EN": "Dutch_English", + # "EN_DE": "English_German", + # "HU_EN": "Hungarian_English", + # "MNI": "Manipuri", + } + self.to_subdl = {v: k for k, v in self.from_subdl.items()} + self.codes = set(self.from_subdl.keys()) + + def convert(self, alpha3, country=None, script=None): + if (alpha3, country, script) in self.to_subdl: + return self.to_subdl[(alpha3, country, script)] + + raise ConfigurationError('Unsupported language for subdl: %s, %s, %s' % (alpha3, country, script)) + + def reverse(self, subdl): + if subdl in self.from_subdl: + return self.from_subdl[subdl] + + raise ConfigurationError('Unsupported language code for subdl: %s' % subdl) diff --git a/custom_libs/subliminal_patch/core.py b/custom_libs/subliminal_patch/core.py index 872896e9c..0fc2ac0a7 100644 --- a/custom_libs/subliminal_patch/core.py +++ b/custom_libs/subliminal_patch/core.py @@ -49,7 +49,17 @@ SUBTITLE_EXTENSIONS = ('.srt', '.sub', '.smi', '.txt', '.ssa', '.ass', '.mpl', ' _POOL_LIFETIME = datetime.timedelta(hours=12) -HI_REGEX = re.compile(r'[*¶♫♪].{3,}[*¶♫♪]|[\[\(\{].{3,}[\]\)\}](? div:nth-child(4) > div.table-responsive > table > tbody')) + .select_one('#content-area > div.block > div.table-responsive > table > tbody')) + + if release_data_table is None: + raise ProviderError('Unexpected HTML page layout - no release data table found') rows = {} for tr in release_data_table.find_all('tr', recursive=False): diff --git a/custom_libs/subliminal_patch/providers/embeddedsubtitles.py b/custom_libs/subliminal_patch/providers/embeddedsubtitles.py index 002f439b7..2d8a492c7 100644 --- a/custom_libs/subliminal_patch/providers/embeddedsubtitles.py +++ b/custom_libs/subliminal_patch/providers/embeddedsubtitles.py @@ -112,7 +112,11 @@ class EmbeddedSubtitlesProvider(Provider): # Default is True container.FFMPEG_STATS = False - tags.LANGUAGE_FALLBACK = self._fallback_lang if self._unknown_as_fallback and self._fallback_lang else None + tags.LANGUAGE_FALLBACK = ( + self._fallback_lang + if self._unknown_as_fallback and self._fallback_lang + else None + ) logger.debug("Language fallback set: %s", tags.LANGUAGE_FALLBACK) def initialize(self): @@ -194,7 +198,7 @@ class EmbeddedSubtitlesProvider(Provider): def download_subtitle(self, subtitle: EmbeddedSubtitle): try: path = self._get_subtitle_path(subtitle) - except KeyError: # TODO: add MustGetBlacklisted support + except KeyError: # TODO: add MustGetBlacklisted support logger.error("Couldn't get subtitle path") return None @@ -229,6 +233,7 @@ class EmbeddedSubtitlesProvider(Provider): timeout=self._timeout, fallback_to_convert=True, basename_callback=_basename_callback, + progress_callback=lambda d: logger.debug("Progress: %s", d), ) # Add the extracted paths to the containter path key self._cached_paths[container.path] = extracted diff --git a/custom_libs/subliminal_patch/providers/hdbits.py b/custom_libs/subliminal_patch/providers/hdbits.py index 19196aecd..87fdfaa82 100644 --- a/custom_libs/subliminal_patch/providers/hdbits.py +++ b/custom_libs/subliminal_patch/providers/hdbits.py @@ -96,7 +96,12 @@ class HDBitsProvider(Provider): "https://hdbits.org/api/torrents", json={**self._def_params, **lookup} ) response.raise_for_status() - ids = [item["id"] for item in response.json()["data"]] + + try: + ids = [item["id"] for item in response.json()["data"]] + except KeyError: + logger.debug("No data found") + return [] subtitles = [] for torrent_id in ids: diff --git a/custom_libs/subliminal_patch/providers/jimaku.py b/custom_libs/subliminal_patch/providers/jimaku.py new file mode 100644 index 000000000..68393821d --- /dev/null +++ b/custom_libs/subliminal_patch/providers/jimaku.py @@ -0,0 +1,419 @@ +from __future__ import absolute_import + +from datetime import timedelta +import logging +import os +import re +import time + +from requests import Session +from subliminal import region, __short_version__ +from subliminal.cache import REFINER_EXPIRATION_TIME +from subliminal.exceptions import ConfigurationError, AuthenticationError, ServiceUnavailable +from subliminal.utils import sanitize +from subliminal.video import Episode, Movie +from subliminal_patch.providers import Provider +from subliminal_patch.subtitle import Subtitle +from subliminal_patch.exceptions import APIThrottled +from subliminal_patch.providers.utils import get_subtitle_from_archive, get_archive_from_bytes +from urllib.parse import urlencode, urljoin +from guessit import guessit +from subzero.language import Language, FULL_LANGUAGE_LIST + +logger = logging.getLogger(__name__) + +# Unhandled formats, such files will always get filtered out +unhandled_archive_formats = (".7z",) +accepted_archive_formats = (".zip", ".rar") + +class JimakuSubtitle(Subtitle): + '''Jimaku Subtitle.''' + provider_name = 'jimaku' + + hash_verifiable = False + + def __init__(self, language, video, download_url, filename): + super(JimakuSubtitle, self).__init__(language, page_link=download_url) + + self.video = video + self.download_url = download_url + self.filename = filename + self.release_info = filename + self.is_archive = filename.endswith(accepted_archive_formats) + + @property + def id(self): + return self.download_url + + def get_matches(self, video): + matches = set() + + # Episode/Movie specific matches + if isinstance(video, Episode): + if sanitize(video.series) and sanitize(self.video.series) in ( + sanitize(name) for name in [video.series] + video.alternative_series): + matches.add('series') + + if video.season and self.video.season is None or video.season and video.season == self.video.season: + matches.add('season') + elif isinstance(video, Movie): + if sanitize(video.title) and sanitize(self.video.title) in ( + sanitize(name) for name in [video.title] + video.alternative_titles): + matches.add('title') + + # General matches + if video.year and video.year == self.video.year: + matches.add('year') + + video_type = 'movie' if isinstance(video, Movie) else 'episode' + matches.add(video_type) + + guess = guessit(self.filename, {'type': video_type}) + for g in guess: + if g[0] == "release_group" or "source": + if video.release_group == g[1]: + matches.add('release_group') + break + + # Prioritize .srt by repurposing the audio_codec match + if self.filename.endswith(".srt"): + matches.add('audio_codec') + + return matches + +class JimakuProvider(Provider): + '''Jimaku Provider.''' + video_types = (Episode, Movie) + + api_url = 'https://jimaku.cc/api' + api_ratelimit_max_delay_seconds = 5 + api_ratelimit_backoff_limit = 3 + + corrupted_file_size_threshold = 500 + + languages = {Language.fromietf("ja")} + + def __init__(self, enable_name_search_fallback, enable_archives_download, enable_ai_subs, api_key): + if api_key: + self.api_key = api_key + else: + raise ConfigurationError('Missing api_key.') + + self.enable_name_search_fallback = enable_name_search_fallback + self.download_archives = enable_archives_download + self.enable_ai_subs = enable_ai_subs + self.session = None + + def initialize(self): + self.session = Session() + self.session.headers['Content-Type'] = 'application/json' + self.session.headers['Authorization'] = self.api_key + self.session.headers['User-Agent'] = os.environ.get("SZ_USER_AGENT") + + def terminate(self): + self.session.close() + + def _query(self, video): + if isinstance(video, Movie): + media_name = video.title.lower() + elif isinstance(video, Episode): + media_name = video.series.lower() + + # With entries that have a season larger than 1, Jimaku appends the corresponding season number to the name. + # We'll reassemble media_name here to account for cases where we can only search by name alone. + season_addendum = str(video.season) if video.season > 1 else None + media_name = f"{media_name} {season_addendum}" if season_addendum else media_name + + # Search for entry + searching_for_entry_attempts = 0 + additional_url_params = {} + while searching_for_entry_attempts < 2: + searching_for_entry_attempts += 1 + url = self._assemble_jimaku_search_url(video, media_name, additional_url_params) + if not url: + return None + + searching_for_entry = "query" in url + data = self._search_for_entry(url) + + if not data: + if searching_for_entry and searching_for_entry_attempts < 2: + logger.info("Maybe this is live action media? Will retry search without anime parameter...") + additional_url_params = {'anime': "false"} + else: + return None + else: + break + + # We only go for the first entry + entry = data[0] + + entry_id = entry.get('id') + anilist_id = entry.get('anilist_id', None) + entry_name = entry.get('name') + is_movie = entry.get('flags', {}).get('movie', False) + + if isinstance(video, Episode) and is_movie: + logger.warn("Bazarr thinks this is a series, but Jimaku says this is a movie! May not be able to match subtitles...") + + logger.info(f"Matched entry: ID: '{entry_id}', anilist_id: '{anilist_id}', name: '{entry_name}', english_name: '{entry.get('english_name')}', movie: {is_movie}") + if entry.get("flags").get("unverified"): + logger.warning(f"This entry '{entry_id}' is unverified, subtitles might be incomplete or have quality issues!") + + # Get a list of subtitles for entry + episode_number = video.episode if "episode" in dir(video) else None + url_params = {'episode': episode_number} if isinstance(video, Episode) and not is_movie else {} + only_look_for_archives = False + + has_offset = isinstance(video, Episode) and video.series_anidb_season_episode_offset is not None + + retry_count = 0 + adjusted_ep_num = None + while retry_count <= 1: + # Account for positive episode offset first + if isinstance(video, Episode) and not is_movie and retry_count < 1: + if video.season > 1 and has_offset: + offset_value = video.series_anidb_season_episode_offset + offset_value = offset_value if offset_value > 0 else -offset_value + + if episode_number < offset_value: + adjusted_ep_num = episode_number + offset_value + logger.warning(f"Will try using adjusted episode number {adjusted_ep_num} first") + url_params = {'episode': adjusted_ep_num} + + url = f"entries/{entry_id}/files" + data = self._search_for_subtitles(url, url_params) + + if not data: + if isinstance(video, Episode) and not is_movie and has_offset and retry_count < 1: + logger.warning(f"Found no subtitles for adjusted episode number, but will retry with normal episode number {episode_number}") + url_params = {'episode': episode_number} + elif isinstance(video, Episode) and not is_movie and retry_count < 1: + logger.warning(f"Found no subtitles for episode number {episode_number}, but will retry without 'episode' parameter") + url_params = {} + only_look_for_archives = True + else: + return None + + retry_count += 1 + else: + if adjusted_ep_num: + video.episode = adjusted_ep_num + logger.debug(f"This videos episode attribute has been updated to: {video.episode}") + break + + # Filter subtitles + list_of_subtitles = [] + + data = [item for item in data if not item['name'].endswith(unhandled_archive_formats)] + + # Detect only archives being uploaded + archive_entries = [item for item in data if item['name'].endswith(accepted_archive_formats)] + subtitle_entries = [item for item in data if not item['name'].endswith(accepted_archive_formats)] + has_only_archives = len(archive_entries) > 0 and len(subtitle_entries) == 0 + if has_only_archives: + logger.warning("Have only found archived subtitles") + + elif only_look_for_archives: + data = [item for item in data if item['name'].endswith(accepted_archive_formats)] + + for item in data: + filename = item.get('name') + download_url = item.get('url') + is_archive = filename.endswith(accepted_archive_formats) + + # Archives will still be considered if they're the only files available, as is mostly the case for movies. + if is_archive and not has_only_archives and not self.download_archives: + logger.warning(f"Skipping archive '{filename}' because normal subtitles are available instead") + continue + + if not self.enable_ai_subs: + p = re.compile(r'[\[\(]?(whisperai)[\]\)]?|[\[\(]whisper[\]\)]', re.IGNORECASE) + if p.search(filename): + logger.warning(f"Skipping subtitle '{filename}' as it's suspected of being AI generated") + continue + + sub_languages = self._try_determine_subtitle_languages(filename) + if len(sub_languages) > 1: + logger.warning(f"Skipping subtitle '{filename}' as it's suspected of containing multiple languages") + continue + + # Check if file is obviously corrupt. If no size is returned, assume OK + filesize = item.get('size', self.corrupted_file_size_threshold) + if filesize < self.corrupted_file_size_threshold: + logger.warning(f"Skipping possibly corrupt file '{filename}': Filesize is just {filesize} bytes") + continue + + if not filename.endswith(unhandled_archive_formats): + lang = sub_languages[0] if len(sub_languages) > 1 else Language("jpn") + list_of_subtitles.append(JimakuSubtitle(lang, video, download_url, filename)) + else: + logger.debug(f"Skipping archive '{filename}' as it's not a supported format") + + return list_of_subtitles + + def list_subtitles(self, video, languages=None): + subtitles = self._query(video) + if not subtitles: + return [] + + return [s for s in subtitles] + + def download_subtitle(self, subtitle: JimakuSubtitle): + target_url = subtitle.download_url + response = self.session.get(target_url, timeout=10) + response.raise_for_status() + + if subtitle.is_archive: + archive = get_archive_from_bytes(response.content) + if archive: + if isinstance(subtitle.video, Episode): + subtitle.content = get_subtitle_from_archive( + archive, + episode=subtitle.video.episode, + episode_title=subtitle.video.title + ) + else: + subtitle.content = get_subtitle_from_archive( + archive + ) + else: + logger.warning("Archive seems to not be an archive! File possibly corrupt?") + return None + else: + subtitle.content = response.content + + def _do_jimaku_request(self, url_path, url_params={}): + url = urljoin(f"{self.api_url}/{url_path}", '?' + urlencode(url_params)) + + retry_count = 0 + while retry_count < self.api_ratelimit_backoff_limit: + response = self.session.get(url, timeout=10) + + if response.status_code == 429: + reset_time = 5 + retry_count + 1 + + logger.warning(f"Jimaku ratelimit hit, waiting for '{reset_time}' seconds ({retry_count}/{self.api_ratelimit_backoff_limit} tries)") + time.sleep(reset_time) + continue + elif response.status_code == 401: + raise AuthenticationError("Unauthorized. API key possibly invalid") + else: + response.raise_for_status() + + data = response.json() + logger.debug(f"Length of response on {url}: {len(data)}") + if len(data) == 0: + logger.error(f"Jimaku returned no items for our our query: {url}") + return None + elif 'error' in data: + raise ServiceUnavailable(f"Jimaku returned an error: '{data.get('error')}', Code: '{data.get('code')}'") + else: + return data + + raise APIThrottled(f"Jimaku ratelimit max backoff limit of {self.api_ratelimit_backoff_limit} reached, aborting") + + # Wrapper functions to indirectly call _do_jimaku_request with different cache configs + @region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME) + def _search_for_entry(self, url_path, url_params={}): + return self._do_jimaku_request(url_path, url_params) + + @region.cache_on_arguments(expiration_time=timedelta(minutes=1).total_seconds()) + def _search_for_subtitles(self, url_path, url_params={}): + return self._do_jimaku_request(url_path, url_params) + + @staticmethod + def _try_determine_subtitle_languages(filename): + # This is more like a guess and not a 100% fool-proof way of detecting multi-lang subs: + # It assumes that language codes, if present, are in the last metadata group of the subs filename. + # If such codes are not present, or we failed to match any at all, then we'll just assume that the sub is purely Japanese. + default_language = Language("jpn") + + dot_delimit = filename.split(".") + bracket_delimit = re.split(r'[\[\]\(\)]+', filename) + + candidate_list = list() + if len(dot_delimit) > 2: + candidate_list = dot_delimit[-2] + elif len(bracket_delimit) > 2: + candidate_list = bracket_delimit[-2] + + candidates = [] if len(candidate_list) == 0 else re.split(r'[,\-\+\& ]+', candidate_list) + + # Discard match group if any candidate... + # ...contains any numbers, as the group is likely encoding information + if any(re.compile(r'\d').search(string) for string in candidates): + return [default_language] + # ...is >= 5 chars long, as the group is likely other unrelated metadata + if any(len(string) >= 5 for string in candidates): + return [default_language] + + languages = list() + for candidate in candidates: + candidate = candidate.lower() + if candidate in ["ass", "srt"]: + continue + + # Sometimes, languages are hidden in 4 character blocks, i.e. "JPSC" + if len(candidate) == 4: + for addendum in [candidate[:2], candidate[2:]]: + candidates.append(addendum) + continue + + # Sometimes, language codes can have additional info such as 'cc' or 'sdh'. For example: "ja[cc]" + if len(dot_delimit) > 2 and any(c in candidate for c in '[]()'): + candidate = re.split(r'[\[\]\(\)]+', candidate)[0] + + try: + language_squash = { + "jp": "ja", + "jap": "ja", + "chs": "zho", + "cht": "zho", + "zhi": "zho", + "cn": "zho" + } + + candidate = language_squash[candidate] if candidate in language_squash else candidate + if len(candidate) > 2: + language = Language(candidate) + else: + language = Language.fromietf(candidate) + + if not any(l.alpha3 == language.alpha3 for l in languages): + languages.append(language) + except: + if candidate in FULL_LANGUAGE_LIST: + # Create a dummy for the unknown language + languages.append(Language("zul")) + + if len(languages) > 1: + # Sometimes a metadata group that actually contains info about codecs gets processed as valid languages. + # To prevent false positives, we'll check if Japanese language codes are in the processed languages list. + # If not, then it's likely that we didn't actually match language codes -> Assume Japanese only subtitle. + contains_jpn = any([l for l in languages if l.alpha3 == "jpn"]) + + return languages if contains_jpn else [Language("jpn")] + else: + return [default_language] + + def _assemble_jimaku_search_url(self, video, media_name, additional_params={}): + endpoint = "entries/search" + anilist_id = video.anilist_id + + params = {} + if anilist_id: + params = {'anilist_id': anilist_id} + else: + if self.enable_name_search_fallback or isinstance(video, Movie): + params = {'query': media_name} + else: + logger.error(f"Skipping '{media_name}': Got no AniList ID and fuzzy matching using name is disabled") + return None + + if additional_params: + params.update(additional_params) + + logger.info(f"Will search for entry based on params: {params}") + return urljoin(endpoint, '?' + urlencode(params)) \ No newline at end of file diff --git a/custom_libs/subliminal_patch/providers/legendasdivx.py b/custom_libs/subliminal_patch/providers/legendasdivx.py index 612693bb9..e4d839ed2 100644 --- a/custom_libs/subliminal_patch/providers/legendasdivx.py +++ b/custom_libs/subliminal_patch/providers/legendasdivx.py @@ -29,6 +29,7 @@ from dogpile.cache.api import NO_VALUE logger = logging.getLogger(__name__) + class LegendasdivxSubtitle(Subtitle): """Legendasdivx Subtitle.""" provider_name = 'legendasdivx' @@ -69,10 +70,12 @@ class LegendasdivxSubtitle(Subtitle): self.wrong_fps = True if self.skip_wrong_fps: - logger.debug("Legendasdivx :: Skipping subtitle due to FPS mismatch (expected: %s, got: %s)", video.fps, self.sub_frame_rate) + logger.debug("Legendasdivx :: Skipping subtitle due to FPS mismatch (expected: %s, got: %s)", video.fps, + self.sub_frame_rate) # not a single match :) return set() - logger.debug("Legendasdivx :: Frame rate mismatch (expected: %s, got: %s, but continuing...)", video.fps, self.sub_frame_rate) + logger.debug("Legendasdivx :: Frame rate mismatch (expected: %s, got: %s, but continuing...)", video.fps, + self.sub_frame_rate) description = sanitize(self.description) @@ -112,6 +115,11 @@ class LegendasdivxSubtitle(Subtitle): matches.update(['season']) if video.episode and 'e{:02d}'.format(video.episode) in description: matches.update(['episode']) + # All the search is already based on the series_imdb_id when present in the video and controlled via the + # the legendasdivx backend it, so if there is a result, it matches, either inside of a pack or a specific + # series and episode, so we can assume the season and episode matches. + if video.series_imdb_id: + matches.update(['series', 'series_imdb_id', 'season', 'episode']) # release_group if video.release_group and sanitize_release_group(video.release_group) in sanitize_release_group(description): @@ -121,6 +129,7 @@ class LegendasdivxSubtitle(Subtitle): return matches + class LegendasdivxProvider(Provider): """Legendasdivx Provider.""" languages = {Language('por', 'BR')} | {Language('por')} @@ -135,7 +144,7 @@ class LegendasdivxProvider(Provider): 'Referer': 'https://www.legendasdivx.pt' } loginpage = site + '/forum/ucp.php?mode=login' - searchurl = site + '/modules.php?name=Downloads&file=jz&d_op=search&op=_jz00&query={query}' + searchurl = site + '/modules.php?name=Downloads&file=jz&d_op={d_op}&op={op}&query={query}&temporada={season}&episodio={episode}&imdb={imdbid}' download_link = site + '/modules.php{link}' def __init__(self, username, password, skip_wrong_fps=True): @@ -186,7 +195,8 @@ class LegendasdivxProvider(Provider): res = self.session.post(self.loginpage, data) res.raise_for_status() # make sure we're logged in - logger.debug('Legendasdivx.pt :: Logged in successfully: PHPSESSID: %s', self.session.cookies.get_dict()['PHPSESSID']) + logger.debug('Legendasdivx.pt :: Logged in successfully: PHPSESSID: %s', + self.session.cookies.get_dict()['PHPSESSID']) cj = self.session.cookies.copy() store_cks = ("PHPSESSID", "phpbb3_2z8zs_sid", "phpbb3_2z8zs_k", "phpbb3_2z8zs_u", "lang") for cn in iter(self.session.cookies.keys()): @@ -252,7 +262,7 @@ class LegendasdivxProvider(Provider): continue # get subtitle uploader - sub_header = _subbox.find("div", {"class" :"sub_header"}) + sub_header = _subbox.find("div", {"class": "sub_header"}) uploader = sub_header.find("a").text if sub_header else 'anonymous' exact_match = False @@ -278,12 +288,24 @@ class LegendasdivxProvider(Provider): subtitles = [] + # Set the default search criteria + d_op = 'search' + op = '_jz00' + + lang_filter_key = 'form_cat' + if isinstance(video, Movie): querytext = video.imdb_id if video.imdb_id else video.title - if isinstance(video, Episode): - querytext = '%22{}%20S{:02d}E{:02d}%22'.format(video.series, video.season, video.episode) - querytext = quote(querytext.lower()) + # Overwrite the parameters to refine via imdb_id + if video.series_imdb_id: + querytext = '&faz=pesquisa_episodio' + lang_filter_key = 'idioma' + d_op = 'jz_00' + op = '' + else: + querytext = '%22{}%22%20S{:02d}E{:02d}'.format(video.series, video.season, video.episode) + querytext = quote(querytext.lower()) # language query filter if not isinstance(languages, (tuple, list, set)): @@ -293,21 +315,30 @@ class LegendasdivxProvider(Provider): logger.debug("Legendasdivx.pt :: searching for %s subtitles.", language) language_id = language.opensubtitles if 'por' in language_id: - lang_filter = '&form_cat=28' + lang_filter = '&{}=28'.format(lang_filter_key) elif 'pob' in language_id: - lang_filter = '&form_cat=29' + lang_filter = '&{}=29'.format(lang_filter_key) else: lang_filter = '' querytext = querytext + lang_filter if lang_filter else querytext + search_url = _searchurl.format( + query=querytext, + season='' if isinstance(video, Movie) else video.season, + episode='' if isinstance(video, Movie) else video.episode, + imdbid='' if isinstance(video, Movie) else video.series_imdb_id.replace('tt', '') if video.series_imdb_id else None, + op=op, + d_op=d_op, + ) + try: # sleep for a 1 second before another request sleep(1) searchLimitReached = False self.headers['Referer'] = self.site + '/index.php' self.session.headers.update(self.headers) - res = self.session.get(_searchurl.format(query=querytext), allow_redirects=False) + res = self.session.get(search_url, allow_redirects=False) res.raise_for_status() if res.status_code == 200 and "', res.text) @@ -327,7 +358,7 @@ class LegendasdivxProvider(Provider): querytext = re.sub(r"(e|E)(\d{2})", "", querytext) # sleep for a 1 second before another request sleep(1) - res = self.session.get(_searchurl.format(query=querytext), allow_redirects=False) + res = self.session.get(search_url, allow_redirects=False) res.raise_for_status() if res.status_code == 200 and "', res.text) @@ -340,9 +371,11 @@ class LegendasdivxProvider(Provider): if searches_count >= self.SAFE_SEARCH_LIMIT: searchLimitReached = True if (res.status_code == 200 and "A legenda não foi encontrada" in res.text): - logger.warning('Legendasdivx.pt :: query {0} return no results for language {1}(for series and season only).'.format(querytext, language_id)) + logger.warning( + 'Legendasdivx.pt :: query {0} return no results for language {1}(for series and season only).'.format( + querytext, language_id)) continue - if res.status_code == 302: # got redirected to login page. + if res.status_code == 302: # got redirected to login page. # seems that our session cookies are no longer valid... clean them from cache region.delete("legendasdivx_cookies2") logger.debug("Legendasdivx.pt :: Logging in again. Cookies have expired!") @@ -350,7 +383,7 @@ class LegendasdivxProvider(Provider): self.login() # sleep for a 1 second before another request sleep(1) - res = self.session.get(_searchurl.format(query=querytext)) + res = self.session.get(search_url, allow_redirects=False) res.raise_for_status() if res.status_code == 200 and "', res.text) @@ -394,9 +427,9 @@ class LegendasdivxProvider(Provider): # more pages? if num_pages > 1: - for num_page in range(2, num_pages+1): + for num_page in range(2, num_pages + 1): sleep(1) # another 1 sec before requesting... - _search_next = self.searchurl.format(query=querytext) + "&page={0}".format(str(num_page)) + _search_next = search_url + "&page={0}".format(str(num_page)) logger.debug("Legendasdivx.pt :: Moving on to next page: %s", _search_next) # sleep for a 1 second before another request sleep(1) @@ -409,7 +442,7 @@ class LegendasdivxProvider(Provider): def list_subtitles(self, video, languages): return self.query(video, languages) - + @reinitialize_on_error((RequestException,), attempts=1) def download_subtitle(self, subtitle): @@ -478,7 +511,8 @@ class LegendasdivxProvider(Provider): if isinstance(subtitle.video, Episode): if all(key in _guess for key in ('season', 'episode')): logger.debug("Legendasdivx.pt :: guessing %s", name) - logger.debug("Legendasdivx.pt :: subtitle S%sE%s video S%sE%s", _guess['season'], _guess['episode'], subtitle.video.season, subtitle.video.episode) + logger.debug("Legendasdivx.pt :: subtitle S%sE%s video S%sE%s", _guess['season'], _guess['episode'], + subtitle.video.season, subtitle.video.episode) if subtitle.video.episode != _guess['episode'] or subtitle.video.season != _guess['season']: logger.debug('Legendasdivx.pt :: subtitle does not match video, skipping') diff --git a/custom_libs/subliminal_patch/providers/legendasnet.py b/custom_libs/subliminal_patch/providers/legendasnet.py new file mode 100644 index 000000000..b5c8e0cd9 --- /dev/null +++ b/custom_libs/subliminal_patch/providers/legendasnet.py @@ -0,0 +1,264 @@ +# -*- coding: utf-8 -*- +import logging +import os +import time +import io +import json + +from zipfile import ZipFile, is_zipfile +from urllib.parse import urljoin +from requests import Session + +from subzero.language import Language +from subliminal import Episode, Movie +from subliminal.exceptions import ConfigurationError, ProviderError, DownloadLimitExceeded +from subliminal_patch.exceptions import APIThrottled +from .mixins import ProviderRetryMixin +from subliminal_patch.subtitle import Subtitle +from subliminal.subtitle import fix_line_ending +from subliminal_patch.providers import Provider +from subliminal_patch.providers import utils + +logger = logging.getLogger(__name__) + +retry_amount = 3 +retry_timeout = 5 + + +class LegendasNetSubtitle(Subtitle): + provider_name = 'legendasnet' + hash_verifiable = False + + def __init__(self, language, forced, page_link, download_link, file_id, release_names, uploader, + season=None, episode=None): + super().__init__(language) + language = Language.rebuild(language, forced=forced) + + self.season = season + self.episode = episode + self.releases = release_names + self.release_info = ', '.join(release_names) + self.language = language + self.forced = forced + self.file_id = file_id + self.page_link = page_link + self.download_link = download_link + self.uploader = uploader + self.matches = None + + @property + def id(self): + return self.file_id + + def get_matches(self, video): + matches = set() + + # handle movies and series separately + if isinstance(video, Episode): + # series + matches.add('series') + # season + if video.season == self.season: + matches.add('season') + # episode + if video.episode == self.episode: + matches.add('episode') + # imdb + matches.add('series_imdb_id') + else: + # title + matches.add('title') + # imdb + matches.add('imdb_id') + + utils.update_matches(matches, video, self.release_info) + + self.matches = matches + + return matches + + +class LegendasNetProvider(ProviderRetryMixin, Provider): + """Legendas.Net Provider""" + server_hostname = 'legendas.net/api' + + languages = {Language('por', 'BR')} + video_types = (Episode, Movie) + + def __init__(self, username, password): + self.session = Session() + self.session.headers = {'User-Agent': os.environ.get("SZ_USER_AGENT", "Sub-Zero/2")} + self.username = username + self.password = password + self.access_token = None + self.video = None + self._started = None + self.login() + + def login(self): + headersList = { + "Accept": "*/*", + "User-Agent": self.session.headers['User-Agent'], + "Content-Type": "application/json" + } + + payload = json.dumps({ + "email": self.username, + "password": self.password + }) + + response = self.session.request("POST", self.server_url() + 'login', data=payload, headers=headersList) + if response.status_code != 200: + raise ConfigurationError('Failed to login and retrieve access token') + self.access_token = response.json().get('access_token') + if not self.access_token: + raise ConfigurationError('Access token not found in login response') + self.session.headers.update({'Authorization': f'Bearer {self.access_token}'}) + + def initialize(self): + self._started = time.time() + + def terminate(self): + self.session.close() + + def server_url(self): + return f'https://{self.server_hostname}/v1/' + + def query(self, languages, video): + self.video = video + + # query the server + if isinstance(self.video, Episode): + res = self.retry( + lambda: self.session.get(self.server_url() + 'search/tv', + json={ + 'name': video.series, + 'page': 1, + 'per_page': 25, + 'tv_episode': video.episode, + 'tv_season': video.season, + 'imdb_id': video.series_imdb_id + }, + headers={'Content-Type': 'application/json'}, + timeout=30), + amount=retry_amount, + retry_timeout=retry_timeout + ) + else: + res = self.retry( + lambda: self.session.get(self.server_url() + 'search/movie', + json={ + 'name': video.title, + 'page': 1, + 'per_page': 25, + 'imdb_id': video.imdb_id + }, + headers={'Content-Type': 'application/json'}, + timeout=30), + amount=retry_amount, + retry_timeout=retry_timeout + ) + + if res.status_code == 404: + logger.error(f"Endpoint not found: {res.url}") + raise ProviderError("Endpoint not found") + elif res.status_code == 429: + raise APIThrottled("Too many requests") + elif res.status_code == 403: + raise ConfigurationError("Invalid access token") + elif res.status_code != 200: + res.raise_for_status() + + subtitles = [] + + result = res.json() + + if ('success' in result and not result['success']) or ('status' in result and not result['status']): + logger.debug(result["error"]) + return [] + + if isinstance(self.video, Episode): + if len(result['tv_shows']): + for item in result['tv_shows']: + subtitle = LegendasNetSubtitle( + language=Language('por', 'BR'), + forced=self._is_forced(item), + page_link=f"https://legendas.net/tv_legenda?movie_id={result['tv_shows'][0]['tmdb_id']}&" + f"legenda_id={item['id']}", + download_link=item['path'], + file_id=item['id'], + release_names=[item.get('release_name', '')], + uploader=item['uploader'], + season=item.get('season', ''), + episode=item.get('episode', '') + ) + subtitle.get_matches(self.video) + if subtitle.language in languages: + subtitles.append(subtitle) + else: + if len(result['movies']): + for item in result['movies']: + subtitle = LegendasNetSubtitle( + language=Language('por', 'BR'), + forced=self._is_forced(item), + page_link=f"https://legendas.net/legenda?movie_id={result['movies'][0]['tmdb_id']}&" + f"legenda_id={item['id']}", + download_link=item['path'], + file_id=item['id'], + release_names=[item.get('release_name', '')], + uploader=item['uploader'], + season=None, + episode=None + ) + subtitle.get_matches(self.video) + if subtitle.language in languages: + subtitles.append(subtitle) + + return subtitles + + @staticmethod + def _is_forced(item): + forced_tags = ['forced', 'foreign'] + for tag in forced_tags: + if tag in item.get('comment', '').lower(): + return True + + # nothing match so we consider it as normal subtitles + return False + + def list_subtitles(self, video, languages): + return self.query(languages, video) + + def download_subtitle(self, subtitle): + logger.debug('Downloading subtitle %r', subtitle) + download_link = urljoin("https://legendas.net", subtitle.download_link) + + r = self.retry( + lambda: self.session.get(download_link, timeout=30), + amount=retry_amount, + retry_timeout=retry_timeout + ) + + if r.status_code == 429: + raise DownloadLimitExceeded("Daily download limit exceeded") + elif r.status_code == 403: + raise ConfigurationError("Invalid access token") + elif r.status_code != 200: + r.raise_for_status() + + if not r: + logger.error(f'Could not download subtitle from {download_link}') + subtitle.content = None + return + else: + archive_stream = io.BytesIO(r.content) + if is_zipfile(archive_stream): + archive = ZipFile(archive_stream) + for name in archive.namelist(): + subtitle_content = archive.read(name) + subtitle.content = fix_line_ending(subtitle_content) + return + else: + subtitle_content = r.content + subtitle.content = fix_line_ending(subtitle_content) + return diff --git a/custom_libs/subliminal_patch/providers/opensubtitlescom.py b/custom_libs/subliminal_patch/providers/opensubtitlescom.py index 83f7f0327..14289919a 100644 --- a/custom_libs/subliminal_patch/providers/opensubtitlescom.py +++ b/custom_libs/subliminal_patch/providers/opensubtitlescom.py @@ -218,7 +218,7 @@ class OpenSubtitlesComProvider(ProviderRetryMixin, Provider): try: self.token = r.json()['token'] - except (ValueError, JSONDecodeError): + except (ValueError, JSONDecodeError, AttributeError): log_request_response(r) raise ProviderError("Cannot get token from provider login response") else: @@ -543,10 +543,6 @@ class OpenSubtitlesComProvider(ProviderRetryMixin, Provider): elif status_code == 429: log_request_response(response) raise TooManyRequests() - elif status_code == 500: - logger.debug("Server side exception raised while downloading from opensubtitles.com website. They " - "should mitigate this soon.") - return None elif status_code == 502: # this one should deal with Bad Gateway issue on their side. raise APIThrottled() diff --git a/custom_libs/subliminal_patch/providers/podnapisi.py b/custom_libs/subliminal_patch/providers/podnapisi.py index 18131ff52..d20accb99 100644 --- a/custom_libs/subliminal_patch/providers/podnapisi.py +++ b/custom_libs/subliminal_patch/providers/podnapisi.py @@ -209,7 +209,8 @@ class PodnapisiProvider(_PodnapisiProvider, ProviderSubtitleArchiveMixin): break # exit if no results - if not xml.find('pagination/results') or not int(xml.find('pagination/results').text): + if (not xml.find('pagination/results') or not xml.find('pagination/results').text or not + int(xml.find('pagination/results').text)): logger.debug('No subtitles found') break diff --git a/custom_libs/subliminal_patch/providers/soustitreseu.py b/custom_libs/subliminal_patch/providers/soustitreseu.py index 945b4a21b..727a70458 100644 --- a/custom_libs/subliminal_patch/providers/soustitreseu.py +++ b/custom_libs/subliminal_patch/providers/soustitreseu.py @@ -277,7 +277,11 @@ class SoustitreseuProvider(Provider, ProviderSubtitleArchiveMixin): release = name[:-4].lower().rstrip('tag').rstrip('en').rstrip('fr') _guess = guessit(release) if isinstance(video, Episode): - if video.episode != _guess['episode'] or video.season != _guess['season']: + try: + if video.episode != _guess['episode'] or video.season != _guess['season']: + continue + except KeyError: + # episode or season are missing from guessit result continue matches = set() diff --git a/custom_libs/subliminal_patch/providers/subdivx.py b/custom_libs/subliminal_patch/providers/subdivx.py index a19ae1b5e..720cba3ed 100644 --- a/custom_libs/subliminal_patch/providers/subdivx.py +++ b/custom_libs/subliminal_patch/providers/subdivx.py @@ -172,7 +172,7 @@ class SubdivxSubtitlesProvider(Provider): logger.debug("Query: %s", query) - response = self.session.post(search_link, data=payload) + response = self.session.post(search_link, data=payload, timeout=30) if response.status_code == 500: logger.debug( diff --git a/custom_libs/subliminal_patch/providers/subdl.py b/custom_libs/subliminal_patch/providers/subdl.py new file mode 100644 index 000000000..102125eae --- /dev/null +++ b/custom_libs/subliminal_patch/providers/subdl.py @@ -0,0 +1,279 @@ +# -*- coding: utf-8 -*- +import logging +import os +import time +import io + +from zipfile import ZipFile, is_zipfile +from urllib.parse import urljoin +from requests import Session + +from babelfish import language_converters +from subzero.language import Language +from subliminal import Episode, Movie +from subliminal.exceptions import ConfigurationError, ProviderError, DownloadLimitExceeded +from subliminal_patch.exceptions import APIThrottled +from .mixins import ProviderRetryMixin +from subliminal_patch.subtitle import Subtitle +from subliminal.subtitle import fix_line_ending +from subliminal_patch.providers import Provider +from subliminal_patch.providers import utils + +logger = logging.getLogger(__name__) + +retry_amount = 3 +retry_timeout = 5 + +language_converters.register('subdl = subliminal_patch.converters.subdl:SubdlConverter') + + +class SubdlSubtitle(Subtitle): + provider_name = 'subdl' + hash_verifiable = False + hearing_impaired_verifiable = True + + def __init__(self, language, forced, hearing_impaired, page_link, download_link, file_id, release_names, uploader, + season=None, episode=None): + super().__init__(language) + language = Language.rebuild(language, hi=hearing_impaired, forced=forced) + + self.season = season + self.episode = episode + self.releases = release_names + self.release_info = ', '.join(release_names) + self.language = language + self.forced = forced + self.hearing_impaired = hearing_impaired + self.file_id = file_id + self.page_link = page_link + self.download_link = download_link + self.uploader = uploader + self.matches = None + + @property + def id(self): + return self.file_id + + def get_matches(self, video): + matches = set() + + # handle movies and series separately + if isinstance(video, Episode): + # series + matches.add('series') + # season + if video.season == self.season: + matches.add('season') + # episode + if video.episode == self.episode: + matches.add('episode') + # imdb + matches.add('series_imdb_id') + else: + # title + matches.add('title') + # imdb + matches.add('imdb_id') + + utils.update_matches(matches, video, self.release_info) + + self.matches = matches + + return matches + + +class SubdlProvider(ProviderRetryMixin, Provider): + """Subdl Provider""" + server_hostname = 'api.subdl.com' + + languages = {Language(*lang) for lang in list(language_converters['subdl'].to_subdl.keys())} + languages.update(set(Language.rebuild(lang, forced=True) for lang in languages)) + languages.update(set(Language.rebuild(l, hi=True) for l in languages)) + + video_types = (Episode, Movie) + + def __init__(self, api_key=None): + if not api_key: + raise ConfigurationError('Api_key must be specified') + + self.session = Session() + self.session.headers = {'User-Agent': os.environ.get("SZ_USER_AGENT", "Sub-Zero/2")} + self.api_key = api_key + self.video = None + self._started = None + + def initialize(self): + self._started = time.time() + + def terminate(self): + self.session.close() + + def server_url(self): + return f'https://{self.server_hostname}/api/v1/' + + def query(self, languages, video): + self.video = video + if isinstance(self.video, Episode): + title = self.video.series + else: + title = self.video.title + + imdb_id = None + if isinstance(self.video, Episode) and self.video.series_imdb_id: + imdb_id = self.video.series_imdb_id + elif isinstance(self.video, Movie) and self.video.imdb_id: + imdb_id = self.video.imdb_id + + # be sure to remove duplicates using list(set()) + langs_list = sorted(list(set([language_converters['subdl'].convert(lang.alpha3, lang.country, lang.script) for + lang in languages]))) + + langs = ','.join(langs_list) + logger.debug(f'Searching for those languages: {langs}') + + # query the server + if isinstance(self.video, Episode): + res = self.retry( + lambda: self.session.get(self.server_url() + 'subtitles', + params=(('api_key', self.api_key), + ('episode_number', self.video.episode), + ('film_name', title if not imdb_id else None), + ('imdb_id', imdb_id if imdb_id else None), + ('languages', langs), + ('season_number', self.video.season), + ('subs_per_page', 30), + ('type', 'tv'), + ('comment', 1), + ('releases', 1), + ('bazarr', 1)), # this argument filter incompatible image based or + # txt subtitles + timeout=30), + amount=retry_amount, + retry_timeout=retry_timeout + ) + else: + res = self.retry( + lambda: self.session.get(self.server_url() + 'subtitles', + params=(('api_key', self.api_key), + ('film_name', title if not imdb_id else None), + ('imdb_id', imdb_id if imdb_id else None), + ('languages', langs), + ('subs_per_page', 30), + ('type', 'movie'), + ('comment', 1), + ('releases', 1), + ('bazarr', 1)), # this argument filter incompatible image based or + # txt subtitles + timeout=30), + amount=retry_amount, + retry_timeout=retry_timeout + ) + + if res.status_code == 429: + raise APIThrottled("Too many requests") + elif res.status_code == 403: + raise ConfigurationError("Invalid API key") + elif res.status_code != 200: + res.raise_for_status() + + subtitles = [] + + result = res.json() + + if ('success' in result and not result['success']) or ('status' in result and not result['status']): + logger.debug(result["error"]) + return [] + + logger.debug(f"Query returned {len(result['subtitles'])} subtitles") + + if len(result['subtitles']): + for item in result['subtitles']: + if item.get('episode_from', False) == item.get('episode_end', False): # ignore season packs + subtitle = SubdlSubtitle( + language=Language.fromsubdl(item['language']), + forced=self._is_forced(item), + hearing_impaired=item.get('hi', False) or self._is_hi(item), + page_link=urljoin("https://subdl.com", item.get('subtitlePage', '')), + download_link=item['url'], + file_id=item['name'], + release_names=item.get('releases', []), + uploader=item.get('author', ''), + season=item.get('season', None), + episode=item.get('episode', None), + ) + subtitle.get_matches(self.video) + if subtitle.language in languages: # make sure only desired subtitles variants are returned + subtitles.append(subtitle) + + return subtitles + + @staticmethod + def _is_hi(item): + # Comments include specific mention of removed or non HI + non_hi_tag = ['hi remove', 'non hi', 'nonhi', 'non-hi', 'non-sdh', 'non sdh', 'nonsdh', 'sdh remove'] + for tag in non_hi_tag: + if tag in item.get('comment', '').lower(): + return False + + # Archive filename include _HI_ + if '_hi_' in item.get('name', '').lower(): + return True + + # Comments or release names include some specific strings + hi_keys = [item.get('comment', '').lower(), [x.lower() for x in item.get('releases', [])]] + hi_tag = ['_hi_', ' hi ', '.hi.', 'hi ', ' hi', 'sdh', '𝓢𝓓𝓗'] + for key in hi_keys: + if any(x in key for x in hi_tag): + return True + + # nothing match so we consider it as non-HI + return False + + @staticmethod + def _is_forced(item): + # Comments include specific mention of forced subtitles + forced_tags = ['forced', 'foreign'] + for tag in forced_tags: + if tag in item.get('comment', '').lower(): + return True + + # nothing match so we consider it as normal subtitles + return False + + def list_subtitles(self, video, languages): + return self.query(languages, video) + + def download_subtitle(self, subtitle): + logger.debug('Downloading subtitle %r', subtitle) + download_link = urljoin("https://dl.subdl.com", subtitle.download_link) + + r = self.retry( + lambda: self.session.get(download_link, timeout=30), + amount=retry_amount, + retry_timeout=retry_timeout + ) + + if r.status_code == 429: + raise DownloadLimitExceeded("Daily download limit exceeded") + elif r.status_code == 403: + raise ConfigurationError("Invalid API key") + elif r.status_code != 200: + r.raise_for_status() + + if not r: + logger.error(f'Could not download subtitle from {download_link}') + subtitle.content = None + return + else: + archive_stream = io.BytesIO(r.content) + if is_zipfile(archive_stream): + archive = ZipFile(archive_stream) + for name in archive.namelist(): + # TODO when possible, deal with season pack / multiple files archive + subtitle_content = archive.read(name) + subtitle.content = fix_line_ending(subtitle_content) + return + else: + logger.error(f'Could not unzip subtitle from {download_link}') + subtitle.content = None + return diff --git a/custom_libs/subliminal_patch/providers/subf2m.py b/custom_libs/subliminal_patch/providers/subf2m.py index 6b3a59129..1ac0b4048 100644 --- a/custom_libs/subliminal_patch/providers/subf2m.py +++ b/custom_libs/subliminal_patch/providers/subf2m.py @@ -132,9 +132,9 @@ _DEFAULT_HEADERS = { class Subf2mProvider(Provider): provider_name = "subf2m" - _movie_title_regex = re.compile(r"^(.+?)( \((\d{4})\))?$") + _movie_title_regex = re.compile(r"^(.+?)(\s+\((\d{4})\))?$") _tv_show_title_regex = re.compile( - r"^(.+?) [-\(]\s?(.*?) (season|series)\)?( \((\d{4})\))?$" + r"^(.+?)\s+[-\(]\s?(.*?)\s+(season|series)\)?(\s+\((\d{4})\))?$" ) _tv_show_title_alt_regex = re.compile(r"(.+)\s(\d{1,2})(?:\s|$)") _supported_languages = {} @@ -220,7 +220,7 @@ class Subf2mProvider(Provider): results = [] for result in self._gen_results(title): - text = result.text.lower() + text = result.text.strip().lower() match = self._movie_title_regex.match(text) if not match: continue @@ -254,7 +254,7 @@ class Subf2mProvider(Provider): results = [] for result in self._gen_results(title): - text = result.text.lower() + text = result.text.strip().lower() match = self._tv_show_title_regex.match(text) if not match: diff --git a/custom_libs/subliminal_patch/providers/supersubtitles.py b/custom_libs/subliminal_patch/providers/supersubtitles.py index c3ecb06a3..cc7752925 100644 --- a/custom_libs/subliminal_patch/providers/supersubtitles.py +++ b/custom_libs/subliminal_patch/providers/supersubtitles.py @@ -455,7 +455,13 @@ class SuperSubtitlesProvider(Provider, ProviderSubtitleArchiveMixin): soup = ParserBeautifulSoup(r, ['lxml']) tables = soup.find_all("table") - tables = tables[0].find_all("tr") + + try: + tables = tables[0].find_all("tr") + except IndexError: + logger.debug("No tables found for %s", url) + return [] + i = 0 for table in tables: diff --git a/custom_libs/subliminal_patch/providers/utils.py b/custom_libs/subliminal_patch/providers/utils.py index 2feaecea2..206334462 100644 --- a/custom_libs/subliminal_patch/providers/utils.py +++ b/custom_libs/subliminal_patch/providers/utils.py @@ -65,7 +65,7 @@ def _get_matching_sub( guess = guessit(sub_name, options=guess_options) matched_episode_num = guess.get("episode") - if matched_episode_num: + if not matched_episode_num: logger.debug("No episode number found in file: %s", sub_name) if episode_title is not None: @@ -86,11 +86,13 @@ def _get_matching_sub( return None -def _analize_sub_name(sub_name: str, title_): - titles = re.split(r"[.-]", os.path.splitext(sub_name)[0]) +def _analize_sub_name(sub_name: str, title_: str): + titles = re.split(r"[\s_\.\+]?[.-][\s_\.\+]?", os.path.splitext(sub_name)[0]) + for title in titles: title = title.strip() - ratio = SequenceMatcher(None, title, title_).ratio() + ratio = SequenceMatcher(None, title.lower(), title_.lower()).ratio() + if ratio > 0.85: logger.debug( "Episode title matched: '%s' -> '%s' [%s]", title, sub_name, ratio diff --git a/custom_libs/subliminal_patch/providers/zimuku.py b/custom_libs/subliminal_patch/providers/zimuku.py index 7c66bfc6a..9fc97eba0 100644 --- a/custom_libs/subliminal_patch/providers/zimuku.py +++ b/custom_libs/subliminal_patch/providers/zimuku.py @@ -316,7 +316,7 @@ class ZimukuProvider(Provider): r = self.yunsuo_bypass(download_link, headers={'Referer': subtitle.page_link}, timeout=30) r.raise_for_status() try: - filename = r.headers["Content-Disposition"] + filename = r.headers["Content-Disposition"].lower() except KeyError: logger.debug("Unable to parse subtitles filename. Dropping this subtitles.") return diff --git a/custom_libs/subliminal_patch/subtitle.py b/custom_libs/subliminal_patch/subtitle.py index 529e99019..c65f8cdd2 100644 --- a/custom_libs/subliminal_patch/subtitle.py +++ b/custom_libs/subliminal_patch/subtitle.py @@ -12,8 +12,9 @@ import chardet import pysrt import pysubs2 from bs4 import UnicodeDammit +from copy import deepcopy from pysubs2 import SSAStyle -from pysubs2.subrip import parse_tags, MAX_REPRESENTABLE_TIME +from pysubs2.formats.subrip import parse_tags, MAX_REPRESENTABLE_TIME from pysubs2.time import ms_to_times from subzero.modification import SubtitleModifications from subzero.language import Language @@ -62,9 +63,14 @@ class Subtitle(Subtitle_): _guessed_encoding = None _is_valid = False use_original_format = False - format = "srt" # default format is srt + # format = "srt" # default format is srt def __init__(self, language, hearing_impaired=False, page_link=None, encoding=None, mods=None, original_format=False): + # language needs to be cloned because it is actually a reference to the provider language object + # if a new copy is not created then all subsequent subtitles for this provider will incorrectly be modified + # at least until Bazarr is restarted or the provider language object is recreated somehow + language = deepcopy(language) + # set subtitle language to hi if it's hearing_impaired if hearing_impaired: language = Language.rebuild(language, hi=True) @@ -74,6 +80,21 @@ class Subtitle(Subtitle_): self.mods = mods self._is_valid = False self.use_original_format = original_format + self._og_format = None + + @property + def format(self): + if self.use_original_format and self._og_format is not None: + logger.debug("Original format requested [%s]", self._og_format) + return self._og_format + + logger.debug("Will assume srt format") + return "srt" + + # Compatibility + @format.setter + def format(self, val): + self._og_format = val def __repr__(self): r_info = str(self.release_info or "").replace("\n", " | ").strip() @@ -260,7 +281,7 @@ class Subtitle(Subtitle_): return encoding def is_valid(self): - """Check if a :attr:`text` is a valid SubRip format. Note that orignal format will pypass the checking + """Check if a :attr:`text` is a valid SubRip format. Note that original format will bypass the checking :return: whether or not the subtitle is valid. :rtype: bool @@ -292,11 +313,13 @@ class Subtitle(Subtitle_): logger.info("Got FPS from MicroDVD subtitle: %s", subs.fps) else: logger.info("Got format: %s", subs.format) - if self.use_original_format: - self.format = subs.format - self._is_valid = True - logger.debug("Using original format") - return True + self._og_format = subs.format + self._is_valid = True + # if self.use_original_format: + # self.format = subs.format + # self._is_valid = True + # logger.debug("Using original format") + return True except pysubs2.UnknownFPSError: # if parsing failed, use frame rate from provider @@ -340,7 +363,7 @@ class Subtitle(Subtitle_): fragment = fragment.replace(r"\n", u"\n") fragment = fragment.replace(r"\N", u"\n") if sty.drawing: - raise pysubs2.ContentNotUsable + return None if format == "srt": if sty.italic: @@ -373,9 +396,10 @@ class Subtitle(Subtitle_): for i, line in enumerate(visible_lines, 1): start = ms_to_timestamp(line.start, mssep=mssep) end = ms_to_timestamp(line.end, mssep=mssep) - try: - text = prepare_text(line.text, sub.styles.get(line.style, SSAStyle.DEFAULT_STYLE)) - except pysubs2.ContentNotUsable: + + text = prepare_text(line.text, sub.styles.get(line.style, SSAStyle.DEFAULT_STYLE)) + + if text is None: continue out.append(u"%d\n" % i) diff --git a/custom_libs/subliminal_patch/video.py b/custom_libs/subliminal_patch/video.py index f5df0c92e..96101cf54 100644 --- a/custom_libs/subliminal_patch/video.py +++ b/custom_libs/subliminal_patch/video.py @@ -35,6 +35,8 @@ class Video(Video_): info_url=None, series_anidb_id=None, series_anidb_episode_id=None, + series_anidb_season_episode_offset=None, + anilist_id=None, **kwargs ): super(Video, self).__init__( @@ -61,3 +63,5 @@ class Video(Video_): self.info_url = info_url self.series_anidb_series_id = series_anidb_id, self.series_anidb_episode_id = series_anidb_episode_id, + self.series_anidb_season_episode_offset = series_anidb_season_episode_offset, + self.anilist_id = anilist_id, diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 65e129bc9..f1418bfdf 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -2,18 +2,24 @@ "rules": { "no-console": "error", "camelcase": "warn", + "no-restricted-imports": [ + "error", + { + "patterns": ["..*"] + } + ], + "simple-import-sort/imports": "error", "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-empty-function": "warn", "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-unused-vars": "warn" }, "extends": [ - "react-app", - "plugin:react-hooks/recommended", "eslint:recommended", + "plugin:react-hooks/recommended", "plugin:@typescript-eslint/recommended" ], - "plugins": ["testing-library"], + "plugins": ["testing-library", "simple-import-sort", "react-refresh"], "overrides": [ { "files": [ @@ -21,6 +27,44 @@ "**/?(*.)+(spec|test).[jt]s?(x)" ], "extends": ["plugin:testing-library/react"] + }, + { + "files": ["*.ts", "*.tsx"], + "rules": { + "simple-import-sort/imports": [ + "error", + { + "groups": [ + [ + // React Packages + "^react", + // Mantine Packages + "^@mantine/", + // Vendor Packages + "^(\\w|@\\w)", + // Side Effect Imports + "^\\u0000", + // Internal Packages + "^@/\\w", + // Parent Imports + "^\\.\\.(?!/?$)", + "^\\.\\./?$", + // Relative Imports + "^\\./(?=.*/)(?!/?$)", + "^\\.(?!/?$)", + "^\\./?$", + // Style Imports + "^.+\\.?(css)$" + ] + ] + } + ] + } } - ] + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "sourceType": "module", + "ecmaVersion": "latest" + } } diff --git a/frontend/.gitignore b/frontend/.gitignore index 28212c025..daeaed3f0 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,7 +1,7 @@ -node_modules -dist *.local +*.tsbuildinfo build coverage - -*.tsbuildinfo +dev-dist +dist +node_modules diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 000000000..973f49d55 --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +20.13 diff --git a/frontend/README.md b/frontend/README.md index 135ccb4f6..b3af2f6f4 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -2,9 +2,12 @@ ## Dependencies -- [Node.js](https://nodejs.org/) +- Either [Node.js](https://nodejs.org/) installed manually or using [Node Version Manager](https://github.com/nvm-sh/nvm) - npm (included in Node.js) +> The recommended Node version to use and maintained is managed on the `.nvmrc` file. You can either install manually +> or use `nvm install` followed by `nvm use`. + ## Getting Started 1. Clone or download this repository diff --git a/frontend/config/chunks.ts b/frontend/config/chunks.ts index c5c5f7fba..6dc7f3772 100644 --- a/frontend/config/chunks.ts +++ b/frontend/config/chunks.ts @@ -1,10 +1,11 @@ +// eslint-disable-next-line no-restricted-imports import { dependencies } from "../package.json"; const vendors = [ "react", "react-router-dom", "react-dom", - "react-query", + "@tanstack/react-query", "axios", "socket.io-client", ]; diff --git a/frontend/index.html b/frontend/index.html index 128ee35bc..46f5375a8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,7 +5,17 @@ - + + + =6.0.0" } }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dev": true, "dependencies": { "@babel/highlight": "^7.24.2", "picocolors": "^1.0.0" @@ -147,33 +166,6 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, - "node_modules/@babel/eslint-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.1.tgz", - "integrity": "sha512-d5guuzMlPeDfZIbpQ8+g1NaCNuAGBBGNECh0HVqz1sjOeVLh2CEaifuOysCH18URW6R7pqXINvf5PaR/dC6jLQ==", - "dev": true, - "dependencies": { - "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || >=14.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0", - "eslint": "^7.5.0 || ^8.0.0" - } - }, - "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/@babel/generator": { "version": "7.24.4", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", @@ -335,6 +327,7 @@ "version": "7.24.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "dev": true, "dependencies": { "@babel/types": "^7.24.0" }, @@ -456,6 +449,7 @@ "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -464,6 +458,7 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -509,6 +504,7 @@ "version": "7.24.2", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", @@ -523,6 +519,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -534,6 +531,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -547,6 +545,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -554,12 +553,14 @@ "node_modules/@babel/highlight/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true }, "node_modules/@babel/highlight/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, "engines": { "node": ">=0.8.0" } @@ -568,6 +569,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, "engines": { "node": ">=4" } @@ -576,6 +578,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -659,109 +662,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.1.tgz", - "integrity": "sha512-zPEvzFijn+hRvJuX2Vu3KbEBN39LN3f7tW3MQO2LsIs57B26KU+kUc82BdAktS1VCM6libzh45eKGI65lg0cpA==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-decorators": "^7.24.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", @@ -813,21 +713,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.1.tgz", - "integrity": "sha512-05RJdO/cCrtVWuAaSn1tS3bH8jbsJa/Y1uD186u6J4C/1mnHFxseeuWpsqr9anvo7TUulev7tm7GDwRV+VuhDw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", @@ -852,21 +737,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-flow": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.1.tgz", - "integrity": "sha512-sxi2kLTI5DeW5vDtMUsk4mTPwvlUDbjOnoWayhynCwrw4QXRld4QEYwqzY8JmQXaJUtgUuCIurtSRH5sn4c7mA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", @@ -921,21 +791,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz", - "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", @@ -1038,21 +893,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz", - "integrity": "sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", @@ -1314,22 +1154,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.24.1.tgz", - "integrity": "sha512-iIYPIWt3dUmUKKE10s3W+jsQ3icFkw0JyRVyY1B7G4yK/nngAOHLVx8xlhA6b/Jzl/Y0nis8gjqhqKtRDQqHWQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-flow": "^7.24.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-for-of": { "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", @@ -1686,55 +1510,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.1.tgz", - "integrity": "sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz", - "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.23.3", - "@babel/types": "^7.23.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", - "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", - "dev": true, - "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.1.tgz", @@ -1765,22 +1540,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.1.tgz", - "integrity": "sha512-+pWEAaDJvSm9aFvJNpLiM2+ktl2Sn2U5DdyiWdZBxmLc6+xGt88dvFqsHiAiDS+8WqUwbDfkKz9jRxK3M0k+kA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz", @@ -1812,26 +1571,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.3.tgz", - "integrity": "sha512-J0BuRPNlNqlMTRJ72eVptpt9VcInbxO6iP3jaxr+1NPhC0UkKL+6oeX6VXMEYdADnuqmMmsBspt4d5w8Y/TCbQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.24.3", - "@babel/helper-plugin-utils": "^7.24.0", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.1", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-shorthand-properties": { "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", @@ -1908,24 +1647,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.4.tgz", - "integrity": "sha512-79t3CQ8+oBGk/80SQ8MN3Bs3obf83zJ0YZjDmDaEZN8MqhMI760apl5z6a20kFeMXBwJX99VpKT8CKxEBp5H1g==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.24.4", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-typescript": "^7.24.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz", @@ -2098,45 +1819,6 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/preset-react": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.1.tgz", - "integrity": "sha512-eFa8up2/8cZXLIpkafhaADTXSnl7IsUFCYenRWrARBz0/qZwcT0RBXpys0LJU4+WfPoF2ZG6ew6s2V6izMCwRA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-transform-react-display-name": "^7.24.1", - "@babel/plugin-transform-react-jsx": "^7.23.4", - "@babel/plugin-transform-react-jsx-development": "^7.22.5", - "@babel/plugin-transform-react-pure-annotations": "^7.24.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.1.tgz", - "integrity": "sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-syntax-jsx": "^7.24.1", - "@babel/plugin-transform-modules-commonjs": "^7.24.1", - "@babel/plugin-transform-typescript": "^7.24.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/regjsgen": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", @@ -2193,6 +1875,7 @@ "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -2208,119 +1891,12 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@emotion/babel-plugin": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", - "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", - "peer": true, - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/serialize": "^1.1.2", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/cache": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", - "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", - "peer": true, - "dependencies": { - "@emotion/memoize": "^0.8.1", - "@emotion/sheet": "^1.2.2", - "@emotion/utils": "^1.2.1", - "@emotion/weak-memoize": "^0.3.1", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==", - "peer": true - }, - "node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", - "peer": true - }, - "node_modules/@emotion/react": { - "version": "11.11.4", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", - "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.11.0", - "@emotion/cache": "^11.11.0", - "@emotion/serialize": "^1.1.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", - "@emotion/utils": "^1.2.1", - "@emotion/weak-memoize": "^0.3.1", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/serialize": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", - "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", - "peer": true, - "dependencies": { - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/unitless": "^0.8.1", - "@emotion/utils": "^1.2.1", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/sheet": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", - "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==", - "peer": true - }, - "node_modules/@emotion/unitless": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", - "peer": true - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", - "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", - "peer": true, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emotion/utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", - "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==", - "peer": true - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", - "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==", - "peer": true + "node_modules/@canvas/image-data": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@canvas/image-data/-/image-data-1.0.0.tgz", + "integrity": "sha512-BxOqI5LgsIQP1odU5KMwV9yoijleOPzHL18/YvNqF9KFSGF2K/DLlYAbDQsWqd/1nbaFuSkYD/191dpMtNh4vw==", + "dev": true, + "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.2", @@ -2791,13 +2367,13 @@ } }, "node_modules/@floating-ui/react": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.19.2.tgz", - "integrity": "sha512-JyNk4A0Ezirq8FlXECvRtQOX/iBe5Ize0W/pLkrZjfHW9GUV7Xnq6zm6fyZuQzaHHqEnVizmvlA96e1/CkZv+w==", + "version": "0.26.12", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.12.tgz", + "integrity": "sha512-D09o62HrWdIkstF2kGekIKAC0/N/Dl6wo3CQsnLcOmO3LkW6Ik8uIb3kw8JYkwxNCcg+uJ2bpWUiIijTBep05w==", "dependencies": { - "@floating-ui/react-dom": "^1.3.0", - "aria-hidden": "^1.1.3", - "tabbable": "^6.0.1" + "@floating-ui/react-dom": "^2.0.0", + "@floating-ui/utils": "^0.2.0", + "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -2805,11 +2381,11 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.3.0.tgz", - "integrity": "sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", "dependencies": { - "@floating-ui/dom": "^1.2.1" + "@floating-ui/dom": "^1.6.1" }, "peerDependencies": { "react": ">=16.8.0", @@ -2828,71 +2404,66 @@ "dev": true }, "node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", - "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", + "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==", "dev": true, - "hasInstallScript": true, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", - "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", + "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", "dev": true, - "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.2" + "@fortawesome/fontawesome-common-types": "6.6.0" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-brands-svg-icons": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.2.tgz", - "integrity": "sha512-zi5FNYdmKLnEc0jc0uuHH17kz/hfYTg4Uei0wMGzcoCL/4d3WM3u1VMc0iGGa31HuhV5i7ZK8ZlTCQrHqRHSGQ==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz", + "integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==", "dev": true, - "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.2" + "@fortawesome/fontawesome-common-types": "6.6.0" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-regular-svg-icons": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.2.tgz", - "integrity": "sha512-iabw/f5f8Uy2nTRtJ13XZTS1O5+t+anvlamJ3zJGLEVE2pKsAWhPv2lq01uQlfgCX7VaveT3EVs515cCN9jRbw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz", + "integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==", "dev": true, - "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.2" + "@fortawesome/fontawesome-common-types": "6.6.0" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz", - "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", + "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", "dev": true, - "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.2" + "@fortawesome/fontawesome-common-types": "6.6.0" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/react-fontawesome": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz", - "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz", + "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==", "dev": true, "dependencies": { "prop-types": "^15.8.1" @@ -3017,6 +2588,17 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", @@ -3034,151 +2616,100 @@ } }, "node_modules/@mantine/core": { - "version": "6.0.21", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-6.0.21.tgz", - "integrity": "sha512-Kx4RrRfv0I+cOCIcsq/UA2aWcYLyXgW3aluAuW870OdXnbII6qg7RW28D+r9D76SHPxWFKwIKwmcucAG08Divg==", + "version": "7.12.2", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.12.2.tgz", + "integrity": "sha512-FrMHOKq4s3CiPIxqZ9xnVX7H4PEGNmbtHMvWO/0YlfPgoV0Er/N/DNJOFW1ys4WSnidPTayYeB41riyxxGOpRQ==", "dependencies": { - "@floating-ui/react": "^0.19.1", - "@mantine/styles": "6.0.21", - "@mantine/utils": "6.0.21", - "@radix-ui/react-scroll-area": "1.0.2", - "react-remove-scroll": "^2.5.5", - "react-textarea-autosize": "8.3.4" + "@floating-ui/react": "^0.26.9", + "clsx": "^2.1.1", + "react-number-format": "^5.3.1", + "react-remove-scroll": "^2.5.7", + "react-textarea-autosize": "8.5.3", + "type-fest": "^4.12.0" }, "peerDependencies": { - "@mantine/hooks": "6.0.21", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "@mantine/hooks": "7.12.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, + "node_modules/@mantine/core/node_modules/type-fest": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.15.0.tgz", + "integrity": "sha512-tB9lu0pQpX5KJq54g+oHOLumOx+pMep4RaM6liXh2PKmVRFF+/vAtUP0ZaJ0kOySfVNjF6doBWPHhBhISKdlIA==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@mantine/dropzone": { - "version": "6.0.21", - "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-6.0.21.tgz", - "integrity": "sha512-v63tL4x7R1CvBNnxJVaVPhBVnQcfROQvyOV0xK/v0ZGNAzFxjJoiCRMGdlBjxnEawM0dRhNs/46ItpBgjQIr6g==", + "version": "7.12.2", + "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-7.12.2.tgz", + "integrity": "sha512-VXKpgFBfRfci6eQEyrmNSsTR7LdtErDhWloVw7W6YRsCqJxJHg9e3luG+yIk+tokzSyLoLOVZRX/mESDEso3PQ==", "dependencies": { - "@mantine/utils": "6.0.21", - "react-dropzone": "14.2.3" + "react-dropzone-esm": "15.0.1" }, "peerDependencies": { - "@mantine/core": "6.0.21", - "@mantine/hooks": "6.0.21", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "@mantine/core": "7.12.2", + "@mantine/hooks": "7.12.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" } }, "node_modules/@mantine/form": { - "version": "6.0.21", - "resolved": "https://registry.npmjs.org/@mantine/form/-/form-6.0.21.tgz", - "integrity": "sha512-d4tlxyZic7MSDnaPx/WliCX1sRFDkUd2nxx4MxxO2T4OSek0YDqTlSBCxeoveu60P+vrQQN5rbbsVsaOJBe4SQ==", + "version": "7.12.2", + "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.12.2.tgz", + "integrity": "sha512-MknzDN5F7u/V24wVrL5VIXNvE7/6NMt40K6w3p7wbKFZiLhdh/tDWdMcRN7PkkWF1j2+eoVCBAOCL74U3BzNag==", "dependencies": { "fast-deep-equal": "^3.1.3", - "klona": "^2.0.5" + "klona": "^2.0.6" }, "peerDependencies": { - "react": ">=16.8.0" + "react": "^18.2.0" } }, "node_modules/@mantine/hooks": { - "version": "6.0.21", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-6.0.21.tgz", - "integrity": "sha512-sYwt5wai25W6VnqHbS5eamey30/HD5dNXaZuaVEAJ2i2bBv8C0cCiczygMDpAFiSYdXoSMRr/SZ2CrrPTzeNew==", + "version": "7.12.2", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.12.2.tgz", + "integrity": "sha512-dVMw8jpM0hAzc8e7/GNvzkk9N0RN/m+PKycETB3H6lJGuXJJSRR4wzzgQKpEhHwPccktDpvb4rkukKDq2jA8Fg==", "peerDependencies": { - "react": ">=16.8.0" + "react": "^18.2.0" } }, "node_modules/@mantine/modals": { - "version": "6.0.21", - "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-6.0.21.tgz", - "integrity": "sha512-Gx2D/ZHMUuYF197JKMWey4K9FeGP9rxYp4lmAEXUrjXiST2fEhLZOdiD75KuOHXd1/sYAU9NcNRo9wXrlF/gUA==", - "dependencies": { - "@mantine/utils": "6.0.21" - }, + "version": "7.12.2", + "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.12.2.tgz", + "integrity": "sha512-ffnu9MtUHceoaLlhrwq+J+eojidEPkq3m2Rrt5HfcZv3vAP8RtqPnTfgk99WOB3vyCtdu8r4I9P3ckuYtPRtAg==", "peerDependencies": { - "@mantine/core": "6.0.21", - "@mantine/hooks": "6.0.21", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "@mantine/core": "7.12.2", + "@mantine/hooks": "7.12.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" } }, "node_modules/@mantine/notifications": { - "version": "6.0.21", - "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-6.0.21.tgz", - "integrity": "sha512-qsrqxuJHK8b67sf9Pfk+xyhvpf9jMsivW8vchfnJfjv7yz1lLvezjytMFp4fMDoYhjHnDPOEc/YFockK4muhOw==", - "dependencies": { - "@mantine/utils": "6.0.21", - "react-transition-group": "4.4.2" - }, - "peerDependencies": { - "@mantine/core": "6.0.21", - "@mantine/hooks": "6.0.21", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@mantine/styles": { - "version": "6.0.21", - "resolved": "https://registry.npmjs.org/@mantine/styles/-/styles-6.0.21.tgz", - "integrity": "sha512-PVtL7XHUiD/B5/kZ/QvZOZZQQOj12QcRs3Q6nPoqaoPcOX5+S7bMZLMH0iLtcGq5OODYk0uxlvuJkOZGoPj8Mg==", + "version": "7.12.2", + "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.12.2.tgz", + "integrity": "sha512-gTvLHkoAZ42v5bZxibP9A50djp5ndEwumVhHSa7mxQ8oSS23tt3It/6hOqH7M+9kHY0a8s+viMiflUzTByA9qg==", "dependencies": { - "clsx": "1.1.1", - "csstype": "3.0.9" + "@mantine/store": "7.12.2", + "react-transition-group": "4.4.5" }, "peerDependencies": { - "@emotion/react": ">=11.9.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@mantine/styles/node_modules/clsx": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", - "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", - "engines": { - "node": ">=6" + "@mantine/core": "7.12.2", + "@mantine/hooks": "7.12.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" } }, - "node_modules/@mantine/styles/node_modules/csstype": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", - "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==" - }, - "node_modules/@mantine/utils": { - "version": "6.0.21", - "resolved": "https://registry.npmjs.org/@mantine/utils/-/utils-6.0.21.tgz", - "integrity": "sha512-33RVDRop5jiWFao3HKd3Yp7A9mEq4HAJxJPTuYm1NkdqX6aTKOQK7wT8v8itVodBp+sb4cJK6ZVdD1UurK/txQ==", + "node_modules/@mantine/store": { + "version": "7.12.2", + "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.12.2.tgz", + "integrity": "sha512-NqL31sO/KcAETEWP/CiXrQOQNoE4168vZsxyXacQHGBueVMJa64WIDQtKLHrCnFRMws3vsXF02/OO4bH4XGcMQ==", "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", - "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", - "dev": true, - "dependencies": { - "eslint-scope": "5.1.1" - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" + "react": "^18.2.0" } }, "node_modules/@nodelib/fs.scandir": { @@ -3222,144 +2753,92 @@ "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", "dev": true }, - "node_modules/@radix-ui/number": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.0.tgz", - "integrity": "sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA==", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@radix-ui/primitive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", - "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", - "dependencies": { - "@babel/runtime": "^7.13.10" + "node_modules/@remix-run/router": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", - "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", + "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10" + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.1", + "is-module": "^1.0.0", + "resolve": "^1.22.1" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", - "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", - "dependencies": { - "@babel/runtime": "^7.13.10" + "engines": { + "node": ">=14.0.0" }, "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.0.tgz", - "integrity": "sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==", - "dependencies": { - "@babel/runtime": "^7.13.10" + "rollup": "^2.78.0||^3.0.0||^4.0.0" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "node_modules/@radix-ui/react-presence": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz", - "integrity": "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==", + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-use-layout-effect": "1.0.0" + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz", - "integrity": "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.1" + "engines": { + "node": ">=14.0.0" }, "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/@radix-ui/react-scroll-area": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.2.tgz", - "integrity": "sha512-k8VseTxI26kcKJaX0HPwkvlNBPTs56JRdYzcZ/vzrNUkDlvXBy8sMc7WvCpYzZkHgb+hd72VW9MqkqecGtuNgg==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/number": "1.0.0", - "@radix-ui/primitive": "1.0.0", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-context": "1.0.0", - "@radix-ui/react-direction": "1.0.0", - "@radix-ui/react-presence": "1.0.0", - "@radix-ui/react-primitive": "1.0.1", - "@radix-ui/react-use-callback-ref": "1.0.0", - "@radix-ui/react-use-layout-effect": "1.0.0" + "rollup": "^2.0.0||^3.0.0||^4.0.0" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", - "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.0" + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", - "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", - "dependencies": { - "@babel/runtime": "^7.13.10" + "engines": { + "node": ">=14.0.0" }, "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", - "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", - "dependencies": { - "@babel/runtime": "^7.13.10" + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "node_modules/@remix-run/router": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", - "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", - "engines": { - "node": ">=14.0.0" - } + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.14.1", @@ -3556,12 +3035,6 @@ "win32" ] }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz", - "integrity": "sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg==", - "dev": true - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3573,6 +3046,117 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, + "node_modules/@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + } + }, + "node_modules/@surma/rollup-plugin-off-main-thread/node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.40.0.tgz", + "integrity": "sha512-eD8K8jsOIq0Z5u/QbvOmfvKKE/XC39jA7yv4hgpl/1SRiU+J8QCIwgM/mEHuunQsL87dcvnHqSVLmf9pD4CiaA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.37.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.37.1.tgz", + "integrity": "sha512-XcG4IIHIv0YQKrexTqo2zogQWR1Sz672tX2KsfE9kzB+9zhx44vRKH5si4WDILE1PIWQpStFs/NnrDQrBAUQpg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.40.1.tgz", + "integrity": "sha512-gOcmu+gpFd2taHrrgMM9RemLYYEDYfsCqszxCC0xtx+csDa4R8t7Hr7SfWXQP13S2sF+mOxySo/+FNXJFYBqcA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.40.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.40.1.tgz", + "integrity": "sha512-/AN2UsbuL+28/KSlBkVHq/4chHTEp4l2UWTKWixXbn4pprLQrZGmQTAKN4tYxZDuNwNZY5+Zp67pDfXj+F/UBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.37.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.40.1", + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.19.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.19.2.tgz", + "integrity": "sha512-itoSIAkA/Vsg+bjY23FSemcTyPhc5/1YjYyaMsr9QSH/cdbZnQxHVWrpWn0Sp2BWN71qkzR7e5ye8WuMmwyOjg==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.19.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.19.2", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.19.2.tgz", + "integrity": "sha512-KpRjhgehIhbfH78ARm/GJDXGnpdw4bCg3qas6yjWSi7czJhI/J6pWln7NHtmBkGE9ZbohiiNtLqwGzKmBfixig==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.0.0.tgz", @@ -3875,12 +3459,6 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true - }, "node_modules/@types/lodash": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.1.tgz", @@ -3896,11 +3474,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" - }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -3908,9 +3481,9 @@ "devOptional": true }, "node_modules/@types/react": { - "version": "18.2.75", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.75.tgz", - "integrity": "sha512-+DNnF7yc5y0bHkBTiLKqXFe+L4B3nvOphiMY3tuA5X10esmjqk7smyBZzbGTy2vsiy/Bnzj8yFIBL8xhRacoOg==", + "version": "18.3.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz", + "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", "devOptional": true, "dependencies": { "@types/prop-types": "*", @@ -3918,22 +3491,20 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.24", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.24.tgz", - "integrity": "sha512-cN6upcKd8zkGy4HU9F1+/s98Hrp6D4MOcippK4PoE8OZRngohHZpbJn1GsaDLz87MqvHNoT13nHvNqM9ocRHZg==", + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "dev": true, "dependencies": { "@types/react": "*" } }, - "node_modules/@types/react-table": { - "version": "7.7.20", - "resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-7.7.20.tgz", - "integrity": "sha512-ahMp4pmjVlnExxNwxyaDrFgmKxSbPwU23sGQw2gJK4EhCvnvmib2s/O/+y1dfV57dXOwpr2plfyBol+vEHbi2w==", + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true, - "dependencies": { - "@types/react": "*" - } + "license": "MIT" }, "node_modules/@types/semver": { "version": "7.5.8", @@ -3947,6 +3518,13 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -3963,32 +3541,32 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", + "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/type-utils": "7.16.0", + "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -3996,26 +3574,140 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", + "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", "dev": true, + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0" }, "engines": { - "node": ">=10" + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", + "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", + "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "lru-cache": "^6.0.0" + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", + "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/typescript-estree": "7.16.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", + "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.16.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -4023,51 +3715,89 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/@typescript-eslint/experimental-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", - "integrity": "sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==", + "node_modules/@typescript-eslint/parser": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", + "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/utils": "5.62.0" + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/typescript-estree": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", + "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", + "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "debug": "^4.3.4" + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", + "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", + "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependenciesMeta": { "typescript": { @@ -4075,6 +3805,63 @@ } } }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", + "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.16.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", @@ -4093,25 +3880,26 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", + "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@typescript-eslint/typescript-estree": "7.16.0", + "@typescript-eslint/utils": "7.16.0", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -4119,6 +3907,147 @@ } } }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", + "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", + "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", + "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", + "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/typescript-estree": "7.16.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", + "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.16.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@typescript-eslint/types": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", @@ -4296,6 +4225,30 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@vite-pwa/assets-generator": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@vite-pwa/assets-generator/-/assets-generator-0.2.4.tgz", + "integrity": "sha512-DXyPLPR/IpbZPSpo1amZEPghY/ziIwpTUKNaz0v1xG+ELzCXmrVQhVzEMqr2JLSqRxjc+UzKfGJA/YdUuaao3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "colorette": "^2.0.20", + "consola": "^3.2.3", + "sharp": "^0.32.6", + "sharp-ico": "^0.1.5", + "unconfig": "^0.3.11" + }, + "bin": { + "pwa-assets-generator": "bin/pwa-assets-generator.mjs" + }, + "engines": { + "node": ">=16.14.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", @@ -4640,17 +4593,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/aria-hidden": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", - "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -4676,26 +4618,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -4705,107 +4627,6 @@ "node": ">=8" } }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.toreversed": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", - "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz", - "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.1.0", - "es-shim-unscopables": "^1.0.2" - } - }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", @@ -4837,23 +4658,26 @@ "node": "*" } }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true, + "license": "MIT" }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "node_modules/attr-accept": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", - "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", "engines": { - "node": ">=4" + "node": ">= 4.0.0" } }, "node_modules/available-typed-arrays": { @@ -4871,15 +4695,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/axe-core": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", - "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/axios": { "version": "1.6.8", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", @@ -4890,28 +4705,12 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/axobject-query": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", - "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", "dev": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } + "license": "Apache-2.0" }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.10", @@ -4952,49 +4751,84 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-plugin-transform-react-remove-prop-types": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", - "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==", + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/babel-preset-react-app": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz", - "integrity": "sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.16.0", - "@babel/plugin-proposal-class-properties": "^7.16.0", - "@babel/plugin-proposal-decorators": "^7.16.4", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", - "@babel/plugin-proposal-numeric-separator": "^7.16.0", - "@babel/plugin-proposal-optional-chaining": "^7.16.0", - "@babel/plugin-proposal-private-methods": "^7.16.0", - "@babel/plugin-transform-flow-strip-types": "^7.16.0", - "@babel/plugin-transform-react-display-name": "^7.16.0", - "@babel/plugin-transform-runtime": "^7.16.4", - "@babel/preset-env": "^7.16.4", - "@babel/preset-react": "^7.16.0", - "@babel/preset-typescript": "^7.16.0", - "@babel/runtime": "^7.16.3", - "babel-plugin-macros": "^3.1.0", - "babel-plugin-transform-react-remove-prop-types": "^0.4.24" + "node_modules/bare-events": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", + "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz", + "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^2.0.0" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "node_modules/bare-os": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.0.tgz", + "integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==", + "dev": true, + "license": "Apache-2.0", + "optional": true }, - "node_modules/big-integer": { - "version": "1.6.52", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", - "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", - "engines": { - "node": ">=0.6" + "node_modules/bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz", + "integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.18.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -5007,42 +4841,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, - "node_modules/broadcast-channel": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", - "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "detect-node": "^2.1.0", - "js-sha3": "0.8.0", - "microseconds": "0.2.0", - "nano-time": "1.0.0", - "oblivious-set": "1.0.0", - "rimraf": "3.0.2", - "unload": "2.2.0" - } - }, "node_modules/browserslist": { "version": "4.23.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", @@ -5075,6 +4906,51 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -5107,10 +4983,20 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "engines": { "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001607", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001607.tgz", @@ -5213,6 +5099,13 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -5229,14 +5122,28 @@ } }, "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", - "dev": true, + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5255,6 +5162,24 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5275,22 +5200,31 @@ "node": ">= 12" } }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/confusing-browser-globals": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "peer": true + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } }, "node_modules/core-js-compat": { "version": "3.36.1", @@ -5305,29 +5239,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "engines": { - "node": ">= 6" - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5342,12 +5253,34 @@ "node": ">= 8" } }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cssstyle": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", @@ -5486,12 +5419,6 @@ "node": ">=12" } }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true - }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -5584,6 +5511,51 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "dev": true }, + "node_modules/decode-bmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/decode-bmp/-/decode-bmp-0.2.1.tgz", + "integrity": "sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@canvas/image-data": "^1.0.0", + "to-data-view": "^1.1.0" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/decode-ico": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-ico/-/decode-ico-0.4.1.tgz", + "integrity": "sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@canvas/image-data": "^1.0.0", + "decode-bmp": "^0.2.0", + "to-data-view": "^1.1.0" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", @@ -5596,12 +5568,32 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -5636,6 +5628,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true, + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5653,10 +5652,15 @@ "node": ">=6" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } }, "node_modules/detect-node-es": { "version": "1.1.0", @@ -5711,50 +5715,51 @@ "csstype": "^3.0.2" } }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.730", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.730.tgz", "integrity": "sha512-oJRPo82XEqtQAobHpJIR3zW5YO3sSRRkPz2an4yxi1UvqhsGm54vR/wzTFV74a3soDOJ8CKW7ajOOX5ESzddwg==", "dev": true }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } }, "node_modules/engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", + "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.0.0" } }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/engine.io-parser": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", @@ -5775,14 +5780,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-abstract": { "version": "1.23.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", @@ -5864,31 +5861,6 @@ "node": ">= 0.4" } }, - "node_modules/es-iterator-helpers": { - "version": "1.0.18", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz", - "integrity": "sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-object-atoms": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", @@ -5915,15 +5887,6 @@ "node": ">= 0.4" } }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - } - }, "node_modules/es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -5992,6 +5955,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "engines": { "node": ">=10" }, @@ -6028,276 +5992,30 @@ "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-react-app": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", - "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.16.0", - "@babel/eslint-parser": "^7.16.3", - "@rushstack/eslint-patch": "^1.1.0", - "@typescript-eslint/eslint-plugin": "^5.5.0", - "@typescript-eslint/parser": "^5.5.0", - "babel-preset-react-app": "^10.0.1", - "confusing-browser-globals": "^1.0.11", - "eslint-plugin-flowtype": "^8.0.3", - "eslint-plugin-import": "^2.25.3", - "eslint-plugin-jest": "^25.3.0", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-react": "^7.27.1", - "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-testing-library": "^5.0.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "eslint": "^8.0.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/eslint-plugin-testing-library": { - "version": "5.11.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz", - "integrity": "sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==", - "dev": true, - "dependencies": { - "@typescript-eslint/utils": "^5.58.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0", - "npm": ">=6" - }, - "peerDependencies": { - "eslint": "^7.5.0 || ^8.0.0" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", - "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", - "dev": true, - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-flowtype": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", - "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", - "dev": true, - "dependencies": { - "lodash": "^4.17.21", - "string-natural-compare": "^3.0.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@babel/plugin-syntax-flow": "^7.14.5", - "@babel/plugin-transform-react-jsx": "^7.14.9", - "eslint": "^8.1.0" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", - "semver": "^6.3.1", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-jest": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", - "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/experimental-utils": "^5.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^4.0.0 || ^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "jest": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", - "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.23.2", - "aria-query": "^5.3.0", - "array-includes": "^3.1.7", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "=4.7.0", - "axobject-query": "^3.2.1", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.0.15", - "hasown": "^2.0.0", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7" - }, - "engines": { - "node": ">=4.0" + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.34.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz", - "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlast": "^1.2.4", - "array.prototype.flatmap": "^1.3.2", - "array.prototype.toreversed": "^1.1.2", - "array.prototype.tosorted": "^1.1.3", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.17", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7", - "object.hasown": "^1.1.3", - "object.values": "^1.1.7", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.10" + "bin": { + "eslint": "bin/eslint.js" }, "engines": { - "node": ">=4" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-plugin-react-hooks": { @@ -6312,33 +6030,24 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz", + "integrity": "sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==", "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" + "license": "MIT", + "peerDependencies": { + "eslint": ">=7" } }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "node_modules/eslint-plugin-simple-import-sort": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.0.tgz", + "integrity": "sha512-Y2fqAfC11TcG/WP3TrI1Gi3p3nc8XJyEOJYHyEPEGI/UAgNx6akxxlX74p7SbAQdLcgASKhj8M0GKvH3vq/+ig==", "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "license": "MIT", + "peerDependencies": { + "eslint": ">=5.0.0" } }, "node_modules/eslint-plugin-testing-library": { @@ -6509,6 +6218,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", @@ -6539,6 +6258,13 @@ "node": ">=6.0.0" } }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -6606,22 +6332,44 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/file-selector": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", - "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", "dependencies": { - "tslib": "^2.4.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">= 12" + "node": ">=10" } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -6629,12 +6377,6 @@ "node": ">=8" } }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "peer": true - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6712,6 +6454,13 @@ "node": ">= 6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, "node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", @@ -6738,7 +6487,8 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -6758,6 +6508,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6834,6 +6585,13 @@ "node": ">=6" } }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true, + "license": "ISC" + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -6863,10 +6621,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -7035,6 +6801,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -7042,15 +6809,6 @@ "node": ">= 0.4" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "peer": true, - "dependencies": { - "react-is": "^16.7.0" - } - }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -7119,6 +6877,13 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/ico-endec": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ico-endec/-/ico-endec-0.1.6.tgz", + "integrity": "sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ==", + "dev": true, + "license": "MPL-2.0" + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -7131,6 +6896,34 @@ "node": ">=0.10.0" } }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -7150,6 +6943,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -7183,6 +6977,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -7191,7 +6986,15 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" }, "node_modules/internal-slot": { "version": "1.0.7", @@ -7240,26 +7043,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, - "node_modules/is-async-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", - "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -7300,6 +7083,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -7316,6 +7115,7 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -7362,33 +7162,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", - "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -7401,17 +7174,12 @@ "node": ">=0.10.0" } }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, "node_modules/is-negative-zero": { "version": "2.0.3", @@ -7429,7 +7197,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -7449,6 +7217,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -7480,16 +7258,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, "node_modules/is-shared-array-buffer": { @@ -7564,18 +7340,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -7588,22 +7352,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -7666,17 +7414,23 @@ "node": ">=8" } }, - "node_modules/iterator.prototype": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", - "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "node_modules/jake": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", + "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/jest-diff": { @@ -7851,10 +7605,15 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/js-sha3": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } }, "node_modules/js-tokens": { "version": "4.0.0", @@ -7931,10 +7690,12 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -7987,19 +7748,14 @@ "node": ">= 10.0.0" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "dev": true, - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, + "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">=0.10.0" } }, "node_modules/keyv": { @@ -8019,22 +7775,14 @@ "node": ">= 8" } }, - "node_modules/language-subtag-registry": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", - "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==", - "dev": true - }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, + "license": "MIT", "engines": { - "node": ">=0.10" + "node": ">=6" } }, "node_modules/levn": { @@ -8050,11 +7798,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, "node_modules/local-pkg": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", @@ -8104,6 +7847,13 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -8213,15 +7963,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/match-sorter": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", - "integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==", - "dependencies": { - "@babel/runtime": "^7.23.8", - "remove-accents": "0.5.0" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -8250,11 +7991,6 @@ "node": ">=8.6" } }, - "node_modules/microseconds": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", - "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -8283,6 +8019,19 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -8296,6 +8045,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -8312,6 +8062,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, "node_modules/mlly": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz", @@ -8347,14 +8104,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/nano-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", - "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==", - "dependencies": { - "big-integer": "^1.6.16" - } - }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -8373,17 +8122,51 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true, + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true + "node_modules/node-abi": { + "version": "3.65.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.65.0.tgz", + "integrity": "sha512-ThjYBfoDNr08AWx6hGaRbfPwxKV9kVzAzOzlLKbk2CuqXE2xnCh+cbAGnwM3t8Lq4v9rUB7VfondlkBckcJrVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT" }, "node_modules/node-releases": { "version": "2.0.14", @@ -8462,95 +8245,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.hasown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", - "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/oblivious-set": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", - "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==" - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "dependencies": { "wrappy": "1" } @@ -8621,6 +8320,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -8628,23 +8328,6 @@ "node": ">=6" } }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -8670,6 +8353,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -8686,12 +8370,14 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, "engines": { "node": ">=8" } @@ -8714,7 +8400,8 @@ "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true }, "node_modules/picomatch": { "version": "2.3.1", @@ -8776,6 +8463,165 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-mixins": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/postcss-mixins/-/postcss-mixins-9.0.4.tgz", + "integrity": "sha512-XVq5jwQJDRu5M1XGkdpgASqLk37OqkH4JCFDXl/Dn7janOJjCTEKL+36cnRVy7bMtoBzALfO7bV7nTIsFnUWLA==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "postcss-js": "^4.0.0", + "postcss-simple-vars": "^7.0.0", + "sugarss": "^4.0.1" + }, + "engines": { + "node": ">=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-preset-mantine": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/postcss-preset-mantine/-/postcss-preset-mantine-1.14.4.tgz", + "integrity": "sha512-T1K3MVhU1hA9mJWfqoGvMcK5WKcHpVi4JUX6AYTbESvp78WneB/KFONUi+eXDG9Lpw62W/KNxEYl1ic3Dpm88w==", + "dev": true, + "dependencies": { + "postcss-mixins": "^9.0.4", + "postcss-nested": "^6.0.1" + }, + "peerDependencies": { + "postcss": ">=8.0.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-simple-vars": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-simple-vars/-/postcss-simple-vars-7.0.1.tgz", + "integrity": "sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A==", + "dev": true, + "engines": { + "node": ">=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8820,6 +8666,19 @@ } } }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -8909,6 +8768,17 @@ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8944,10 +8814,53 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true, + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -8956,24 +8869,22 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.1" } }, - "node_modules/react-dropzone": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", - "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", + "node_modules/react-dropzone-esm": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/react-dropzone-esm/-/react-dropzone-esm-15.0.1.tgz", + "integrity": "sha512-RdeGpqwHnoV/IlDFpQji7t7pTtlC2O1i/Br0LWkRZ9hYtLyce814S71h5NolnCZXsIN5wrZId6+8eQj2EBnEzg==", "dependencies": { - "attr-accept": "^2.2.2", - "file-selector": "^0.6.0", "prop-types": "^15.8.1" }, "engines": { @@ -8988,29 +8899,16 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/react-query": { - "version": "3.39.3", - "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz", - "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==", + "node_modules/react-number-format": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.3.4.tgz", + "integrity": "sha512-2hHN5mbLuCDUx19bv0Q8wet67QqYK6xmtLQeY5xx+h7UXiMmRtaCwqko4mMPoKXLc6xAzwRrutg8XbTRlsfjRg==", "dependencies": { - "@babel/runtime": "^7.5.5", - "broadcast-channel": "^3.4.1", - "match-sorter": "^6.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "prop-types": "^15.7.2" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } + "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/react-refresh": { @@ -9068,11 +8966,11 @@ } }, "node_modules/react-router": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", - "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", "dependencies": { - "@remix-run/router": "1.15.3" + "@remix-run/router": "1.16.1" }, "engines": { "node": ">=14.0.0" @@ -9082,12 +8980,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", - "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", "dependencies": { - "@remix-run/router": "1.15.3", - "react-router": "6.22.3" + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" }, "engines": { "node": ">=14.0.0" @@ -9107,25 +9005,9 @@ "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-smooth/node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/react-style-singleton": { @@ -9150,25 +9032,12 @@ } } }, - "node_modules/react-table": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz", - "integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.3 || ^17.0.0-0 || ^18.0.0" - } - }, "node_modules/react-textarea-autosize": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.3.4.tgz", - "integrity": "sha512-CdtmP8Dc19xL8/R6sWvtknD/eCXkQr30dtvC4VmGInhRsfF8X/ihXCq6+9l9qbxmKRiq407/7z5fxE7cVWQNgQ==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz", + "integrity": "sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==", "dependencies": { - "@babel/runtime": "^7.10.2", + "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, @@ -9180,9 +9049,9 @@ } }, "node_modules/react-transition-group": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", - "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", @@ -9194,6 +9063,21 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -9207,9 +9091,9 @@ } }, "node_modules/recharts": { - "version": "2.12.6", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.6.tgz", - "integrity": "sha512-D+7j9WI+D0NHauah3fKHuNNcRK8bOypPW7os1DERinogGBGaHI7i6tQKJ0aUF3JXyBZ63dyfKIW2WTOPJDxJ8w==", + "version": "2.12.7", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz", + "integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==", "dev": true, "dependencies": { "clsx": "^2.0.0", @@ -9251,27 +9135,6 @@ "node": ">=8" } }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", - "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.1", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -9360,10 +9223,15 @@ "jsesc": "bin/jsesc" } }, - "node_modules/remove-accents": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", - "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/requires-port": { "version": "1.0.0", @@ -9375,6 +9243,7 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -9391,6 +9260,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "engines": { "node": ">=4" } @@ -9409,6 +9279,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -9500,6 +9371,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -9553,9 +9445,9 @@ } }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dependencies": { "loose-envify": "^1.1.0" } @@ -9569,6 +9461,16 @@ "semver": "bin/semver.js" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -9601,6 +9503,55 @@ "node": ">= 0.4" } }, + "node_modules/sharp": { + "version": "0.32.6", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", + "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.1", + "semver": "^7.5.4", + "simple-get": "^4.0.1", + "tar-fs": "^3.0.4", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp-ico": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/sharp-ico/-/sharp-ico-0.1.5.tgz", + "integrity": "sha512-a3jODQl82NPp1d5OYb0wY+oFaPk7AvyxipIowCHk7pBsZCWgbe0yAkU2OOXdoH0ENyANhyOQbs9xkAiRHcF02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "decode-ico": "*", + "ico-endec": "*", + "sharp": "*" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9652,6 +9603,70 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true, + "license": "MIT" + }, "node_modules/sirv": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", @@ -9675,6 +9690,13 @@ "node": ">=8" } }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "dev": true, + "license": "MIT" + }, "node_modules/socket.io-client": { "version": "4.7.5", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", @@ -9702,12 +9724,16 @@ } }, "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "peer": true, + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, "node_modules/source-map-js": { @@ -9719,6 +9745,64 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/source-map/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/source-map/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -9752,11 +9836,30 @@ "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, - "node_modules/string-natural-compare": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", - "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==", - "dev": true + "node_modules/streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } }, "node_modules/string.prototype.matchall": { "version": "4.0.11", @@ -9833,6 +9936,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -9845,13 +9963,14 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=10" } }, "node_modules/strip-final-newline": { @@ -9905,11 +10024,21 @@ "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", "dev": true }, - "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", - "peer": true + "node_modules/sugarss": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-4.0.1.tgz", + "integrity": "sha512-WCjS5NfuVJjkQzK10s8WOBY+hhDxxNt/N6ZaGwxFZ+wN3/lKKFSaaKUNecULcTTvE4urLcKaZFQD8vO0mOZujw==", + "dev": true, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } }, "node_modules/supports-color": { "version": "7.2.0", @@ -9927,6 +10056,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -9945,6 +10075,101 @@ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, + "node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.31.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz", + "integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -9959,6 +10184,16 @@ "node": ">=8" } }, + "node_modules/text-decoder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", + "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -9995,10 +10230,18 @@ "node": ">=14.0.0" } }, + "node_modules/to-data-view": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-1.1.0.tgz", + "integrity": "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==", + "dev": true, + "license": "MIT" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, "engines": { "node": ">=4" } @@ -10007,7 +10250,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -10051,28 +10294,17 @@ "node": ">=18" } }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, - "dependencies": { - "minimist": "^1.2.0" + "license": "MIT", + "engines": { + "node": ">=16" }, - "bin": { - "json5": "lib/cli.js" + "peerDependencies": { + "typescript": ">=4.2.0" } }, "node_modules/tslib": { @@ -10101,6 +10333,19 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -10241,6 +10486,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unconfig": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/unconfig/-/unconfig-0.3.13.tgz", + "integrity": "sha512-N9Ph5NC4+sqtcOjPfHrRcHekBCadCXWTBzp2VYYbySOHW0PfD9XLCeXshTXjkPYwLrBr9AtSeU0CZmkYECJhng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.7", + "defu": "^6.1.4", + "jiti": "^1.21.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -10287,6 +10547,19 @@ "node": ">=4" } }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -10296,13 +10569,15 @@ "node": ">= 4.0.0" } }, - "node_modules/unload": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", - "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", - "dependencies": { - "@babel/runtime": "^7.6.2", - "detect-node": "^2.0.4" + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" } }, "node_modules/update-browserslist-db": { @@ -10432,6 +10707,12 @@ } } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", @@ -10647,6 +10928,37 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/vite-plugin-pwa": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.20.0.tgz", + "integrity": "sha512-/kDZyqF8KqoXRpMUQtR5Atri/7BWayW8Gp7Kz/4bfstsV6zSFTxjREbXZYL7zSuRL40HGA+o2hvUAFRmC+bL7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "pretty-bytes": "^6.1.1", + "workbox-build": "^7.1.0", + "workbox-window": "^7.1.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vite-pwa/assets-generator": "^0.2.4", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0", + "workbox-build": "^7.1.0", + "workbox-window": "^7.1.0" + }, + "peerDependenciesMeta": { + "@vite-pwa/assets-generator": { + "optional": true + } + } + }, "node_modules/vitest": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.4.0.tgz", @@ -11028,50 +11340,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-builtin-type": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", - "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", - "dev": true, - "dependencies": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", - "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", - "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/which-typed-array": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", @@ -11107,16 +11375,401 @@ "node": ">=8" } }, + "node_modules/workbox-background-sync": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.1.0.tgz", + "integrity": "sha512-rMbgrzueVWDFcEq1610YyDW71z0oAXLfdRHRQcKw4SGihkfOK0JUEvqWHFwA6rJ+6TClnMIn7KQI5PNN1XQXwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.1.0" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.1.0.tgz", + "integrity": "sha512-O36hIfhjej/c5ar95pO67k1GQw0/bw5tKP7CERNgK+JdxBANQhDmIuOXZTNvwb2IHBx9hj2kxvcDyRIh5nzOgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.1.0" + } + }, + "node_modules/workbox-build": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.1.1.tgz", + "integrity": "sha512-WdkVdC70VMpf5NBCtNbiwdSZeKVuhTEd5PV3mAwpTQCGAB5XbOny1P9egEgNdetv4srAMmMKjvBk4RD58LpooA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.24.4", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^2.4.1", + "@rollup/plugin-terser": "^0.4.3", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^7.1.6", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.43.1", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "7.1.0", + "workbox-broadcast-update": "7.1.0", + "workbox-cacheable-response": "7.1.0", + "workbox-core": "7.1.0", + "workbox-expiration": "7.1.0", + "workbox-google-analytics": "7.1.0", + "workbox-navigation-preload": "7.1.0", + "workbox-precaching": "7.1.0", + "workbox-range-requests": "7.1.0", + "workbox-recipes": "7.1.0", + "workbox-routing": "7.1.0", + "workbox-strategies": "7.1.0", + "workbox-streams": "7.1.0", + "workbox-sw": "7.1.0", + "workbox-window": "7.1.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/workbox-build/node_modules/@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + } + } + }, + "node_modules/workbox-build/node_modules/@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/workbox-build/node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/workbox-build/node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-build/node_modules/ajv": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", + "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/workbox-build/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-build/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/workbox-build/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-build/node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/workbox-build/node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/workbox-build/node_modules/rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/workbox-build/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.1.0.tgz", + "integrity": "sha512-iwsLBll8Hvua3xCuBB9h92+/e0wdsmSVgR2ZlvcfjepZWwhd3osumQB3x9o7flj+FehtWM2VHbZn8UJeBXXo6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.1.0" + } + }, + "node_modules/workbox-core": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.1.0.tgz", + "integrity": "sha512-5KB4KOY8rtL31nEF7BfvU7FMzKT4B5TkbYa2tzkS+Peqj0gayMT9SytSFtNzlrvMaWgv6y/yvP9C0IbpFjV30Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-expiration": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.1.0.tgz", + "integrity": "sha512-m5DcMY+A63rJlPTbbBNtpJ20i3enkyOtSgYfv/l8h+D6YbbNiA0zKEkCUaMsdDlxggla1oOfRkyqTvl5Ni5KQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.1.0" + } + }, + "node_modules/workbox-google-analytics": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.1.0.tgz", + "integrity": "sha512-FvE53kBQHfVTcZyczeBVRexhh7JTkyQ8HAvbVY6mXd2n2A7Oyz/9fIwnY406ZcDhvE4NFfKGjW56N4gBiqkrew==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-background-sync": "7.1.0", + "workbox-core": "7.1.0", + "workbox-routing": "7.1.0", + "workbox-strategies": "7.1.0" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.1.0.tgz", + "integrity": "sha512-4wyAbo0vNI/X0uWNJhCMKxnPanNyhybsReMGN9QUpaePLTiDpKxPqFxl4oUmBNddPwIXug01eTSLVIFXimRG/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.1.0" + } + }, + "node_modules/workbox-precaching": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.1.0.tgz", + "integrity": "sha512-LyxzQts+UEpgtmfnolo0hHdNjoB7EoRWcF7EDslt+lQGd0lW4iTvvSe3v5JiIckQSB5KTW5xiCqjFviRKPj1zA==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.1.0", + "workbox-routing": "7.1.0", + "workbox-strategies": "7.1.0" + } + }, + "node_modules/workbox-range-requests": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.1.0.tgz", + "integrity": "sha512-m7+O4EHolNs5yb/79CrnwPR/g/PRzMFYEdo01LqwixVnc/sbzNSvKz0d04OE3aMRel1CwAAZQheRsqGDwATgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.1.0" + } + }, + "node_modules/workbox-recipes": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.1.0.tgz", + "integrity": "sha512-NRrk4ycFN9BHXJB6WrKiRX3W3w75YNrNrzSX9cEZgFB5ubeGoO8s/SDmOYVrFYp9HMw6sh1Pm3eAY/1gVS8YLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-cacheable-response": "7.1.0", + "workbox-core": "7.1.0", + "workbox-expiration": "7.1.0", + "workbox-precaching": "7.1.0", + "workbox-routing": "7.1.0", + "workbox-strategies": "7.1.0" + } + }, + "node_modules/workbox-routing": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.1.0.tgz", + "integrity": "sha512-oOYk+kLriUY2QyHkIilxUlVcFqwduLJB7oRZIENbqPGeBP/3TWHYNNdmGNhz1dvKuw7aqvJ7CQxn27/jprlTdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.1.0" + } + }, + "node_modules/workbox-strategies": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.1.0.tgz", + "integrity": "sha512-/UracPiGhUNehGjRm/tLUQ+9PtWmCbRufWtV0tNrALuf+HZ4F7cmObSEK+E4/Bx1p8Syx2tM+pkIrvtyetdlew==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.1.0" + } + }, + "node_modules/workbox-streams": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.1.0.tgz", + "integrity": "sha512-WyHAVxRXBMfysM8ORwiZnI98wvGWTVAq/lOyBjf00pXFvG0mNaVz4Ji+u+fKa/mf1i2SnTfikoYKto4ihHeS6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.1.0", + "workbox-routing": "7.1.0" + } + }, + "node_modules/workbox-sw": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.1.0.tgz", + "integrity": "sha512-Hml/9+/njUXBglv3dtZ9WBKHI235AQJyLBV1G7EFmh4/mUdSQuXui80RtjDeVRrXnm/6QWgRUEHG3/YBVbxtsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-window": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.1.0.tgz", + "integrity": "sha512-ZHeROyqR+AS5UPzholQRDttLFqGMwP0Np8MKWAdyxsDETxq3qOAyXvqessc3GniohG6e0mAqSQyKOHmT8zPF7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "7.1.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", - "dev": true, + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/frontend/package.json b/frontend/package.json index eadb26496..57a8cdd55 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,70 +13,81 @@ }, "private": true, "dependencies": { - "@mantine/core": "^6.0.21", - "@mantine/dropzone": "^6.0.21", - "@mantine/form": "^6.0.21", - "@mantine/hooks": "^6.0.21", - "@mantine/modals": "^6.0.21", - "@mantine/notifications": "^6.0.21", + "@mantine/core": "^7.12.2", + "@mantine/dropzone": "^7.12.2", + "@mantine/form": "^7.12.2", + "@mantine/hooks": "^7.12.2", + "@mantine/modals": "^7.12.2", + "@mantine/notifications": "^7.12.2", + "@tanstack/react-query": "^5.40.1", + "@tanstack/react-table": "^8.19.2", "axios": "^1.6.8", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-query": "^3.39.3", - "react-router-dom": "^6.22.3", + "braces": "^3.0.3", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.23.1", "socket.io-client": "^4.7.5" }, "devDependencies": { "@fontsource/roboto": "^5.0.12", - "@fortawesome/fontawesome-svg-core": "^6.5.2", - "@fortawesome/free-brands-svg-icons": "^6.5.2", - "@fortawesome/free-regular-svg-icons": "^6.5.2", - "@fortawesome/free-solid-svg-icons": "^6.5.2", - "@fortawesome/react-fontawesome": "^0.2.0", + "@fortawesome/fontawesome-svg-core": "^6.6.0", + "@fortawesome/free-brands-svg-icons": "^6.6.0", + "@fortawesome/free-regular-svg-icons": "^6.6.0", + "@fortawesome/free-solid-svg-icons": "^6.6.0", + "@fortawesome/react-fontawesome": "^0.2.2", + "@tanstack/react-query-devtools": "^5.40.1", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^15.0.5", "@testing-library/user-event": "^14.5.2", "@types/jest": "^29.5.12", "@types/lodash": "^4.17.1", "@types/node": "^20.12.6", - "@types/react": "^18.2.75", - "@types/react-dom": "^18.2.24", - "@types/react-table": "^7.7.20", + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.16.0", + "@typescript-eslint/parser": "^7.16.0", + "@vite-pwa/assets-generator": "^0.2.4", "@vitejs/plugin-react": "^4.2.1", "@vitest/coverage-v8": "^1.4.0", "@vitest/ui": "^1.2.2", "clsx": "^2.1.0", "eslint": "^8.57.0", - "eslint-config-react-app": "^7.0.1", "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.7", + "eslint-plugin-simple-import-sort": "^12.1.0", "eslint-plugin-testing-library": "^6.2.0", "husky": "^9.0.11", "jsdom": "^24.0.0", "lodash": "^4.17.21", + "postcss-preset-mantine": "^1.14.4", + "postcss-simple-vars": "^7.0.1", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^3.2.4", "pretty-quick": "^4.0.0", - "react-table": "^7.8.0", - "recharts": "^2.12.6", + "recharts": "^2.12.7", "sass": "^1.74.1", "typescript": "^5.4.4", "vite": "^5.2.8", "vite-plugin-checker": "^0.6.4", + "vite-plugin-pwa": "^0.20.0", "vitest": "^1.2.2", "yaml": "^2.4.1" }, "scripts": { - "start": "vite", "build": "vite build", "build:ci": "vite build -m development", "check": "eslint --ext .ts,.tsx src", + "check:fix": "eslint --ext .ts,.tsx src --fix", "check:ts": "tsc --noEmit --incremental false", "check:fmt": "prettier -c .", - "test": "vitest", - "test:ui": "vitest --ui", "coverage": "vitest run --coverage", "format": "prettier -w .", - "prepare": "cd .. && husky frontend/.husky" + "pwa-assets:generate": "pwa-assets-generator --preset minimal-2023 public/images/logo128.png", + "prepare": "cd .. && husky frontend/.husky", + "preview": "vite preview", + "start": "vite", + "test": "vitest", + "test:ui": "vitest --ui" }, "browserslist": { "production": [ diff --git a/frontend/postcss.config.cjs b/frontend/postcss.config.cjs new file mode 100644 index 000000000..e817f567b --- /dev/null +++ b/frontend/postcss.config.cjs @@ -0,0 +1,14 @@ +module.exports = { + plugins: { + "postcss-preset-mantine": {}, + "postcss-simple-vars": { + variables: { + "mantine-breakpoint-xs": "36em", + "mantine-breakpoint-sm": "48em", + "mantine-breakpoint-md": "62em", + "mantine-breakpoint-lg": "75em", + "mantine-breakpoint-xl": "88em", + }, + }, + }, +}; diff --git a/frontend/public/images/apple-touch-icon-180x180.png b/frontend/public/images/apple-touch-icon-180x180.png new file mode 100644 index 000000000..01c19a6ee Binary files /dev/null and b/frontend/public/images/apple-touch-icon-180x180.png differ diff --git a/frontend/public/images/apple-touch-icon.png b/frontend/public/images/apple-touch-icon.png deleted file mode 100644 index c9cc483a1..000000000 Binary files a/frontend/public/images/apple-touch-icon.png and /dev/null differ diff --git a/frontend/public/images/maskable-icon-512x512.png b/frontend/public/images/maskable-icon-512x512.png new file mode 100644 index 000000000..e813d5ddf Binary files /dev/null and b/frontend/public/images/maskable-icon-512x512.png differ diff --git a/frontend/public/images/pwa-192x192.png b/frontend/public/images/pwa-192x192.png new file mode 100644 index 000000000..a93a96a65 Binary files /dev/null and b/frontend/public/images/pwa-192x192.png differ diff --git a/frontend/public/images/pwa-512x512.png b/frontend/public/images/pwa-512x512.png new file mode 100644 index 000000000..298f27dba Binary files /dev/null and b/frontend/public/images/pwa-512x512.png differ diff --git a/frontend/public/images/pwa-64x64.png b/frontend/public/images/pwa-64x64.png new file mode 100644 index 000000000..41487e49c Binary files /dev/null and b/frontend/public/images/pwa-64x64.png differ diff --git a/frontend/public/images/pwa-narrow-series-list.jpeg b/frontend/public/images/pwa-narrow-series-list.jpeg new file mode 100644 index 000000000..890e86a31 Binary files /dev/null and b/frontend/public/images/pwa-narrow-series-list.jpeg differ diff --git a/frontend/public/images/pwa-narrow-series-overview.jpeg b/frontend/public/images/pwa-narrow-series-overview.jpeg new file mode 100644 index 000000000..b7ab53feb Binary files /dev/null and b/frontend/public/images/pwa-narrow-series-overview.jpeg differ diff --git a/frontend/public/images/pwa-wide-series-list.jpeg b/frontend/public/images/pwa-wide-series-list.jpeg new file mode 100644 index 000000000..938081a1e Binary files /dev/null and b/frontend/public/images/pwa-wide-series-list.jpeg differ diff --git a/frontend/public/images/pwa-wide-series-overview.jpeg b/frontend/public/images/pwa-wide-series-overview.jpeg new file mode 100644 index 000000000..56ad0abe8 Binary files /dev/null and b/frontend/public/images/pwa-wide-series-overview.jpeg differ diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json deleted file mode 100644 index 010638054..000000000 --- a/frontend/public/manifest.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "Bazarr", - "short_name": "Bazarr", - "description": "Bazarr is a companion application to Sonarr and Radarr. It manages and downloads subtitles based on your requirements.", - "start_url": "/", - "display": "standalone", - "theme_color": "#be4bdb", - "background_color": "#ffffff", - "icons": [ - { - "src": "/images/android-chrome-96x96.png", - "sizes": "96x96", - "type": "image/png" - }, - { - "src": "/images/apple-touch-icon.png", - "sizes": "180x180", - "type": "image/png" - }, - { - "src": "/images/mstile-150x150.png", - "sizes": "150x150", - "type": "image/png" - } - ] -} \ No newline at end of file diff --git a/frontend/src/App/Header.module.scss b/frontend/src/App/Header.module.scss new file mode 100644 index 000000000..85b3661a9 --- /dev/null +++ b/frontend/src/App/Header.module.scss @@ -0,0 +1,9 @@ +.header { + @include light { + color: var(--mantine-color-gray-0); + } + + @include dark { + color: var(--mantine-color-dark-0); + } +} diff --git a/frontend/src/App/Header.tsx b/frontend/src/App/Header.tsx index c15071045..29c1d1a8d 100644 --- a/frontend/src/App/Header.tsx +++ b/frontend/src/App/Header.tsx @@ -1,38 +1,26 @@ -import { useSystem, useSystemSettings } from "@/apis/hooks"; -import { Action, Search } from "@/components"; -import { Layout } from "@/constants"; -import { useNavbar } from "@/contexts/Navbar"; -import { useIsOnline } from "@/contexts/Online"; -import { Environment, useGotoHomepage } from "@/utilities"; -import { - faArrowRotateLeft, - faGear, - faPowerOff, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { FunctionComponent } from "react"; import { Anchor, + AppShell, Avatar, Badge, Burger, Divider, Group, - Header, - MediaQuery, Menu, - createStyles, } from "@mantine/core"; -import { FunctionComponent } from "react"; - -const useStyles = createStyles((theme) => { - const headerBackgroundColor = - theme.colorScheme === "light" ? theme.colors.gray[0] : theme.colors.dark[4]; - return { - header: { - backgroundColor: headerBackgroundColor, - }, - }; -}); +import { + faArrowRotateLeft, + faGear, + faPowerOff, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useSystem, useSystemSettings } from "@/apis/hooks"; +import { Action, Search } from "@/components"; +import { useNavbar } from "@/contexts/Navbar"; +import { useIsOnline } from "@/contexts/Online"; +import { Environment, useGotoHomepage } from "@/utilities"; +import styles from "./Header.module.scss"; const AppHeader: FunctionComponent = () => { const { data: settings } = useSystemSettings(); @@ -47,39 +35,28 @@ const AppHeader: FunctionComponent = () => { const goHome = useGotoHomepage(); - const { classes } = useStyles(); - return ( - - - - - - - - - - show(!showed)} - size="sm" - > - - + + + + + + + show(!showed)} + size="sm" + hiddenFrom="sm" + > + Bazarr - + @@ -87,21 +64,20 @@ const AppHeader: FunctionComponent = () => { label="System" tooltip={{ position: "left", openDelay: 2000 }} loading={offline} - color={offline ? "yellow" : undefined} + c={offline ? "yellow" : undefined} icon={faGear} size="lg" - variant="light" > } + leftSection={} onClick={() => restart()} > Restart } + leftSection={} onClick={() => shutdown()} > Shutdown @@ -114,7 +90,7 @@ const AppHeader: FunctionComponent = () => { - + ); }; diff --git a/frontend/src/App/Navbar.module.scss b/frontend/src/App/Navbar.module.scss new file mode 100644 index 000000000..ddb444e4d --- /dev/null +++ b/frontend/src/App/Navbar.module.scss @@ -0,0 +1,56 @@ +.anchor { + border-color: var(--mantine-color-gray-5); + text-decoration: none; + + @include dark { + border-color: var(--mantine-color-dark-5); + } + + &.active { + border-left: 2px solid $color-brand-4; + background-color: var(--mantine-color-gray-1); + + @include dark { + border-left: 2px solid $color-brand-8; + background-color: var(--mantine-color-dark-8); + } + } + + &.hover { + background-color: var(--mantine-color-gray-0); + + @include dark { + background-color: var(--mantine-color-dark-7); + } + } +} + +.badge { + margin-left: auto; + text-decoration: none; + box-shadow: var(--mantine-shadow-xs); +} + +.icon { + width: 1.4rem; + margin-right: var(--mantine-spacing-xs); +} + +.nav { + background-color: var(--mantine-color-gray-2); + + @include dark { + background-color: var(--mantine-color-dark-8); + } +} + +.text { + display: inline-flex; + align-items: center; + width: 100%; + color: var(--mantine-color-gray-8); + + @include dark { + color: var(--mantine-color-gray-5); + } +} diff --git a/frontend/src/App/Navbar.tsx b/frontend/src/App/Navbar.tsx index c626dc257..679a0e3e7 100644 --- a/frontend/src/App/Navbar.tsx +++ b/frontend/src/App/Navbar.tsx @@ -1,40 +1,40 @@ -import { Action } from "@/components"; -import { Layout } from "@/constants"; -import { useNavbar } from "@/contexts/Navbar"; -import { useRouteItems } from "@/Router"; -import { CustomRouteObject, Route } from "@/Router/type"; -import { BuildKey, pathJoin } from "@/utilities"; -import { LOG } from "@/utilities/console"; -import { - faHeart, - faMoon, - faSun, - IconDefinition, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { + createContext, + FunctionComponent, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import { matchPath, NavLink, RouteObject, useLocation } from "react-router-dom"; import { Anchor, + AppShell, Badge, Collapse, - createStyles, Divider, Group, - Navbar as MantineNavbar, Stack, Text, + useComputedColorScheme, useMantineColorScheme, } from "@mantine/core"; import { useHover } from "@mantine/hooks"; -import clsx from "clsx"; import { - createContext, - FunctionComponent, - useContext, - useEffect, - useMemo, - useState, -} from "react"; -import { matchPath, NavLink, RouteObject, useLocation } from "react-router-dom"; + faHeart, + faMoon, + faSun, + IconDefinition, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import clsx from "clsx"; +import { Action } from "@/components"; +import { useNavbar } from "@/contexts/Navbar"; +import { useRouteItems } from "@/Router"; +import { CustomRouteObject, Route } from "@/Router/type"; +import { BuildKey, pathJoin } from "@/utilities"; +import { LOG } from "@/utilities/console"; +import styles from "./Navbar.module.scss"; const Selection = createContext<{ selection: string | null; @@ -97,11 +97,12 @@ function useIsActive(parent: string, route: RouteObject) { } const AppNavbar: FunctionComponent = () => { - const { showed } = useNavbar(); const [selection, select] = useState(null); - const { colorScheme, toggleColorScheme } = useMantineColorScheme(); - const dark = colorScheme === "dark"; + const { toggleColorScheme } = useMantineColorScheme(); + const computedColorScheme = useComputedColorScheme("light"); + + const dark = computedColorScheme === "dark"; const routes = useRouteItems(); @@ -111,23 +112,10 @@ const AppNavbar: FunctionComponent = () => { }, [pathname]); return ( - ({ - root: { - backgroundColor: - theme.colorScheme === "light" - ? theme.colors.gray[2] - : theme.colors.dark[6], - }, - })} - > + - - + + {routes.map((route, idx) => ( { > ))} - + - - + + toggleColorScheme()} icon={dark ? faSun : faMoon} > @@ -151,17 +138,12 @@ const AppNavbar: FunctionComponent = () => { href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=XHHRWXT9YB7WE&source=url" target="_blank" > - + - + - + ); }; @@ -186,7 +168,7 @@ const RouteItem: FunctionComponent<{ if (children !== undefined) { const elements = ( - + {children.map((child, idx) => ( + { - const borderColor = - theme.colorScheme === "light" ? theme.colors.gray[5] : theme.colors.dark[4]; - - const activeBorderColor = - theme.colorScheme === "light" - ? theme.colors.brand[4] - : theme.colors.brand[8]; - - const activeBackgroundColor = - theme.colorScheme === "light" ? theme.colors.gray[1] : theme.colors.dark[8]; - - const hoverBackgroundColor = - theme.colorScheme === "light" ? theme.colors.gray[0] : theme.colors.dark[7]; - - const textColor = - theme.colorScheme === "light" ? theme.colors.gray[8] : theme.colors.gray[5]; - - return { - text: { - display: "inline-flex", - alignItems: "center", - width: "100%", - color: textColor, - }, - anchor: { - textDecoration: "none", - borderLeft: `2px solid ${borderColor}`, - }, - active: { - backgroundColor: activeBackgroundColor, - borderLeft: `2px solid ${activeBorderColor}`, - boxShadow: theme.shadows.xs, - }, - hover: { - backgroundColor: hoverBackgroundColor, - }, - icon: { width: "1.4rem", marginRight: theme.spacing.xs }, - badge: { - marginLeft: "auto", - textDecoration: "none", - boxShadow: theme.shadows.xs, - color: textColor, - }, - }; -}); - interface NavbarItemProps { name: string; link: string; @@ -308,8 +243,6 @@ const NavbarItem: FunctionComponent = ({ onClick, primary = false, }) => { - const { classes } = useStyles(); - const { show } = useNavbar(); const { ref, hovered } = useHover(); @@ -335,9 +268,9 @@ const NavbarItem: FunctionComponent = ({ }} className={({ isActive }) => clsx( - clsx(classes.anchor, { - [classes.active]: isActive, - [classes.hover]: hovered, + clsx(styles.anchor, { + [styles.active]: isActive, + [styles.hover]: hovered, }), ) } @@ -347,18 +280,19 @@ const NavbarItem: FunctionComponent = ({ inline p="xs" size="sm" - weight={primary ? "bold" : "normal"} - className={classes.text} + fw={primary ? "bold" : "normal"} + className={styles.text} + span > {icon && ( )} {name} - {shouldHideBadge === false && ( - + {!shouldHideBadge && ( + {badge} )} diff --git a/frontend/src/App/ThemeLoader.tsx b/frontend/src/App/ThemeLoader.tsx new file mode 100644 index 000000000..2bc7e4005 --- /dev/null +++ b/frontend/src/App/ThemeLoader.tsx @@ -0,0 +1,39 @@ +import { useCallback, useEffect, useState } from "react"; +import { MantineColorScheme, useMantineColorScheme } from "@mantine/core"; +import { useSystemSettings } from "@/apis/hooks"; + +const ThemeProvider = () => { + const [localScheme, setLocalScheme] = useState( + null, + ); + const { setColorScheme } = useMantineColorScheme(); + + const settings = useSystemSettings(); + + const settingsColorScheme = settings.data?.general + .theme as MantineColorScheme; + + const setScheme = useCallback( + (colorScheme: MantineColorScheme) => { + setColorScheme(colorScheme); + }, + [setColorScheme], + ); + + useEffect(() => { + if (!settingsColorScheme) { + return; + } + + if (localScheme === settingsColorScheme) { + return; + } + + setScheme(settingsColorScheme); + setLocalScheme(settingsColorScheme); + }, [settingsColorScheme, setScheme, localScheme]); + + return <>>; +}; + +export default ThemeProvider; diff --git a/frontend/src/App/ThemeProvider.tsx b/frontend/src/App/ThemeProvider.tsx new file mode 100644 index 000000000..662a1ce69 --- /dev/null +++ b/frontend/src/App/ThemeProvider.tsx @@ -0,0 +1,61 @@ +import { FunctionComponent, PropsWithChildren } from "react"; +import { + ActionIcon, + Badge, + Button, + createTheme, + MantineProvider, + Pagination, +} from "@mantine/core"; +import ThemeLoader from "@/App/ThemeLoader"; +import "@mantine/core/styles.layer.css"; +import "@mantine/notifications/styles.layer.css"; +import styleVars from "@/assets/_variables.module.scss"; +import actionIconClasses from "@/assets/action_icon.module.scss"; +import badgeClasses from "@/assets/badge.module.scss"; +import buttonClasses from "@/assets/button.module.scss"; +import paginationClasses from "@/assets/pagination.module.scss"; + +const themeProvider = createTheme({ + fontFamily: "Roboto, open sans, Helvetica Neue, Helvetica, Arial, sans-serif", + colors: { + brand: [ + styleVars.colorBrand0, + styleVars.colorBrand1, + styleVars.colorBrand2, + styleVars.colorBrand3, + styleVars.colorBrand4, + styleVars.colorBrand5, + styleVars.colorBrand6, + styleVars.colorBrand7, + styleVars.colorBrand8, + styleVars.colorBrand9, + ], + }, + primaryColor: "brand", + components: { + ActionIcon: ActionIcon.extend({ + classNames: actionIconClasses, + }), + Badge: Badge.extend({ + classNames: badgeClasses, + }), + Button: Button.extend({ + classNames: buttonClasses, + }), + Pagination: Pagination.extend({ + classNames: paginationClasses, + }), + }, +}); + +const ThemeProvider: FunctionComponent = ({ children }) => { + return ( + + + {children} + + ); +}; + +export default ThemeProvider; diff --git a/frontend/src/App/app.test.tsx b/frontend/src/App/app.test.tsx index f6236cdc9..db9895305 100644 --- a/frontend/src/App/app.test.tsx +++ b/frontend/src/App/app.test.tsx @@ -1,5 +1,5 @@ -import { render } from "@/tests"; import { describe, it } from "vitest"; +import { render } from "@/tests"; import App from "."; describe("App", () => { diff --git a/frontend/src/App/index.tsx b/frontend/src/App/index.tsx index 4e09a97da..a8ef9f3fb 100644 --- a/frontend/src/App/index.tsx +++ b/frontend/src/App/index.tsx @@ -1,18 +1,18 @@ +import { FunctionComponent, useEffect, useState } from "react"; +import { Outlet, useNavigate } from "react-router-dom"; +import { AppShell } from "@mantine/core"; +import { useWindowEvent } from "@mantine/hooks"; +import { showNotification } from "@mantine/notifications"; import AppNavbar from "@/App/Navbar"; -import { RouterNames } from "@/Router/RouterNames"; import ErrorBoundary from "@/components/ErrorBoundary"; -import { Layout } from "@/constants"; import NavbarProvider from "@/contexts/Navbar"; import OnlineProvider from "@/contexts/Online"; import { notification } from "@/modules/task"; import CriticalError from "@/pages/errors/CriticalError"; +import { RouterNames } from "@/Router/RouterNames"; import { Environment } from "@/utilities"; -import { AppShell } from "@mantine/core"; -import { useWindowEvent } from "@mantine/hooks"; -import { showNotification } from "@mantine/notifications"; -import { FunctionComponent, useEffect, useState } from "react"; -import { Outlet, useNavigate } from "react-router-dom"; import AppHeader from "./Header"; +import styleVars from "@/assets/_variables.module.scss"; const App: FunctionComponent = () => { const navigate = useNavigate(); @@ -55,13 +55,19 @@ const App: FunctionComponent = () => { } - navbar={} + navbar={{ + width: styleVars.navBarWidth, + breakpoint: "sm", + collapsed: { mobile: !navbar }, + }} + header={{ height: { base: styleVars.headerHeight } }} padding={0} - fixed > - + + + + + diff --git a/frontend/src/App/theme.tsx b/frontend/src/App/theme.tsx deleted file mode 100644 index 947b4f7a8..000000000 --- a/frontend/src/App/theme.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useSystemSettings } from "@/apis/hooks"; -import { - ColorScheme, - ColorSchemeProvider, - createEmotionCache, - MantineProvider, - MantineThemeOverride, -} from "@mantine/core"; -import { useColorScheme } from "@mantine/hooks"; -import { - FunctionComponent, - PropsWithChildren, - useCallback, - useEffect, - useState, -} from "react"; - -const theme: MantineThemeOverride = { - fontFamily: "Roboto, open sans, Helvetica Neue, Helvetica, Arial, sans-serif", - colors: { - brand: [ - "#F8F0FC", - "#F3D9FA", - "#EEBEFA", - "#E599F7", - "#DA77F2", - "#CC5DE8", - "#BE4BDB", - "#AE3EC9", - "#9C36B5", - "#862E9C", - ], - }, - primaryColor: "brand", -}; - -function useAutoColorScheme() { - const settings = useSystemSettings(); - const settingsColorScheme = settings.data?.general.theme; - - let preferredColorScheme: ColorScheme = useColorScheme(); - switch (settingsColorScheme) { - case "light": - preferredColorScheme = "light" as ColorScheme; - break; - case "dark": - preferredColorScheme = "dark" as ColorScheme; - break; - } - - const [colorScheme, setColorScheme] = useState(preferredColorScheme); - - // automatically switch dark/light theme - useEffect(() => { - setColorScheme(preferredColorScheme); - }, [preferredColorScheme]); - - const toggleColorScheme = useCallback((value?: ColorScheme) => { - setColorScheme((scheme) => value || (scheme === "dark" ? "light" : "dark")); - }, []); - - return { colorScheme, setColorScheme, toggleColorScheme }; -} - -const emotionCache = createEmotionCache({ key: "bazarr" }); - -const ThemeProvider: FunctionComponent = ({ children }) => { - const { colorScheme, toggleColorScheme } = useAutoColorScheme(); - - return ( - - - {children} - - - ); -}; - -export default ThemeProvider; diff --git a/frontend/src/Router/Redirector.tsx b/frontend/src/Router/Redirector.tsx index 064522bbc..07878c9db 100644 --- a/frontend/src/Router/Redirector.tsx +++ b/frontend/src/Router/Redirector.tsx @@ -1,7 +1,7 @@ -import { useSystemSettings } from "@/apis/hooks"; -import { LoadingOverlay } from "@mantine/core"; import { FunctionComponent, useEffect } from "react"; import { useNavigate } from "react-router-dom"; +import { LoadingOverlay } from "@mantine/core"; +import { useSystemSettings } from "@/apis/hooks"; const Redirector: FunctionComponent = () => { const { data } = useSystemSettings(); diff --git a/frontend/src/Router/index.tsx b/frontend/src/Router/index.tsx index 335cf2d75..d600fc87d 100644 --- a/frontend/src/Router/index.tsx +++ b/frontend/src/Router/index.tsx @@ -1,11 +1,29 @@ -import App from "@/App"; +import { + createContext, + FunctionComponent, + lazy, + useContext, + useMemo, +} from "react"; +import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import { + faClock, + faCogs, + faExclamationTriangle, + faFileExcel, + faFilm, + faLaptop, + faPlay, +} from "@fortawesome/free-solid-svg-icons"; import { useBadges } from "@/apis/hooks"; import { useEnabledStatus } from "@/apis/hooks/site"; +import App from "@/App"; import { Lazy } from "@/components/async"; import Authentication from "@/pages/Authentication"; import BlacklistMoviesView from "@/pages/Blacklist/Movies"; import BlacklistSeriesView from "@/pages/Blacklist/Series"; import Episodes from "@/pages/Episodes"; +import NotFound from "@/pages/errors/NotFound"; import MoviesHistoryView from "@/pages/History/Movies"; import SeriesHistoryView from "@/pages/History/Series"; import MovieView from "@/pages/Movies"; @@ -30,30 +48,14 @@ import SystemReleasesView from "@/pages/System/Releases"; import SystemTasksView from "@/pages/System/Tasks"; import WantedMoviesView from "@/pages/Wanted/Movies"; import WantedSeriesView from "@/pages/Wanted/Series"; -import NotFound from "@/pages/errors/NotFound"; import { Environment } from "@/utilities"; -import { - faClock, - faCogs, - faExclamationTriangle, - faFileExcel, - faFilm, - faLaptop, - faPlay, -} from "@fortawesome/free-solid-svg-icons"; -import { - FunctionComponent, - createContext, - lazy, - useContext, - useMemo, -} from "react"; -import { RouterProvider, createBrowserRouter } from "react-router-dom"; import Redirector from "./Redirector"; import { RouterNames } from "./RouterNames"; import { CustomRouteObject } from "./type"; -const HistoryStats = lazy(() => import("@/pages/History/Statistics")); +const HistoryStats = lazy( + () => import("@/pages/History/Statistics/HistoryStats"), +); const SystemStatusView = lazy(() => import("@/pages/System/Status")); function useRoutes(): CustomRouteObject[] { diff --git a/frontend/src/Router/type.d.ts b/frontend/src/Router/type.d.ts index 0276a30dd..f1cfdaae7 100644 --- a/frontend/src/Router/type.d.ts +++ b/frontend/src/Router/type.d.ts @@ -1,5 +1,5 @@ -import { IconDefinition } from "@fortawesome/free-solid-svg-icons"; import { RouteObject } from "react-router-dom"; +import { IconDefinition } from "@fortawesome/free-solid-svg-icons"; declare namespace Route { export type Item = { diff --git a/frontend/src/apis/hooks/episodes.ts b/frontend/src/apis/hooks/episodes.ts index d182dc7e6..956fd103f 100644 --- a/frontend/src/apis/hooks/episodes.ts +++ b/frontend/src/apis/hooks/episodes.ts @@ -1,12 +1,13 @@ +import { useEffect } from "react"; import { QueryClient, useMutation, useQuery, useQueryClient, -} from "react-query"; -import { usePaginationQuery } from "../queries/hooks"; -import { QueryKeys } from "../queries/keys"; -import api from "../raw"; +} from "@tanstack/react-query"; +import { usePaginationQuery } from "@/apis/queries/hooks"; +import { QueryKeys } from "@/apis/queries/keys"; +import api from "@/apis/raw"; const cacheEpisodes = (client: QueryClient, episodes: Item.Episode[]) => { episodes.forEach((item) => { @@ -24,30 +25,21 @@ const cacheEpisodes = (client: QueryClient, episodes: Item.Episode[]) => { }); }; -export function useEpisodesByIds(ids: number[]) { - const client = useQueryClient(); - return useQuery( - [QueryKeys.Series, QueryKeys.Episodes, ids], - () => api.episodes.byEpisodeId(ids), - { - onSuccess: (data) => { - cacheEpisodes(client, data); - }, - }, - ); -} - export function useEpisodesBySeriesId(id: number) { const client = useQueryClient(); - return useQuery( - [QueryKeys.Series, id, QueryKeys.Episodes, QueryKeys.All], - () => api.episodes.bySeriesId([id]), - { - onSuccess: (data) => { - cacheEpisodes(client, data); - }, - }, - ); + + const query = useQuery({ + queryKey: [QueryKeys.Series, id, QueryKeys.Episodes, QueryKeys.All], + queryFn: () => api.episodes.bySeriesId([id]), + }); + + useEffect(() => { + if (query.isSuccess && query.data) { + cacheEpisodes(client, query.data); + } + }, [query.isSuccess, query.data, client]); + + return query; } export function useEpisodeWantedPagination() { @@ -57,17 +49,18 @@ export function useEpisodeWantedPagination() { } export function useEpisodeBlacklist() { - return useQuery( - [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist], - () => api.episodes.blacklist(), - ); + return useQuery({ + queryKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist], + queryFn: () => api.episodes.blacklist(), + }); } export function useEpisodeAddBlacklist() { const client = useQueryClient(); - return useMutation( - [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist], - (param: { + return useMutation({ + mutationKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist], + + mutationFn: (param: { seriesId: number; episodeId: number; form: FormType.AddBlacklist; @@ -75,35 +68,33 @@ export function useEpisodeAddBlacklist() { const { seriesId, episodeId, form } = param; return api.episodes.addBlacklist(seriesId, episodeId, form); }, - { - onSuccess: (_, { seriesId, episodeId }) => { - client.invalidateQueries([ - QueryKeys.Series, - QueryKeys.Episodes, - QueryKeys.Blacklist, - ]); - client.invalidateQueries([QueryKeys.Series, seriesId]); - }, + + onSuccess: (_, { seriesId }) => { + void client.invalidateQueries({ + queryKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist], + }); + + void client.invalidateQueries({ + queryKey: [QueryKeys.Series, seriesId], + }); }, - ); + }); } export function useEpisodeDeleteBlacklist() { const client = useQueryClient(); - return useMutation( - [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist], - (param: { all?: boolean; form?: FormType.DeleteBlacklist }) => + return useMutation({ + mutationKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist], + + mutationFn: (param: { all?: boolean; form?: FormType.DeleteBlacklist }) => api.episodes.deleteBlacklist(param.all, param.form), - { - onSuccess: (_, param) => { - client.invalidateQueries([ - QueryKeys.Series, - QueryKeys.Episodes, - QueryKeys.Blacklist, - ]); - }, + + onSuccess: () => { + void client.invalidateQueries({ + queryKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist], + }); }, - ); + }); } export function useEpisodeHistoryPagination() { @@ -115,12 +106,20 @@ export function useEpisodeHistoryPagination() { } export function useEpisodeHistory(episodeId?: number) { - return useQuery( - [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.History, episodeId], - () => { + return useQuery({ + queryKey: [ + QueryKeys.Series, + QueryKeys.Episodes, + QueryKeys.History, + episodeId, + ], + + queryFn: () => { if (episodeId) { return api.episodes.historyBy(episodeId); } + + return []; }, - ); + }); } diff --git a/frontend/src/apis/hooks/histories.ts b/frontend/src/apis/hooks/histories.ts index d8fc0676f..6368d11db 100644 --- a/frontend/src/apis/hooks/histories.ts +++ b/frontend/src/apis/hooks/histories.ts @@ -1,6 +1,6 @@ -import { useQuery } from "react-query"; -import { QueryKeys } from "../queries/keys"; -import api from "../raw"; +import { useQuery } from "@tanstack/react-query"; +import { QueryKeys } from "@/apis/queries/keys"; +import api from "@/apis/raw"; export function useHistoryStats( time: History.TimeFrameOptions, @@ -8,14 +8,19 @@ export function useHistoryStats( provider: System.Provider | null, language: Language.Info | null, ) { - return useQuery( - [QueryKeys.System, QueryKeys.History, { time, action, provider, language }], - () => + return useQuery({ + queryKey: [ + QueryKeys.System, + QueryKeys.History, + { time, action, provider, language }, + ], + + queryFn: () => api.history.stats( time, action ?? undefined, provider?.name, language?.code2, ), - ); + }); } diff --git a/frontend/src/apis/hooks/languages.ts b/frontend/src/apis/hooks/languages.ts index 149d80716..4cbdce69b 100644 --- a/frontend/src/apis/hooks/languages.ts +++ b/frontend/src/apis/hooks/languages.ts @@ -1,23 +1,19 @@ -import { useQuery } from "react-query"; -import { QueryKeys } from "../queries/keys"; -import api from "../raw"; +import { useQuery } from "@tanstack/react-query"; +import { QueryKeys } from "@/apis/queries/keys"; +import api from "@/apis/raw"; export function useLanguages(history?: boolean) { - return useQuery( - [QueryKeys.System, QueryKeys.Languages, history ?? false], - () => api.system.languages(history), - { - staleTime: Infinity, - }, - ); + return useQuery({ + queryKey: [QueryKeys.System, QueryKeys.Languages, history ?? false], + queryFn: () => api.system.languages(history), + staleTime: Infinity, + }); } export function useLanguageProfiles() { - return useQuery( - [QueryKeys.System, QueryKeys.LanguagesProfiles], - () => api.system.languagesProfileList(), - { - staleTime: Infinity, - }, - ); + return useQuery({ + queryKey: [QueryKeys.System, QueryKeys.LanguagesProfiles], + queryFn: () => api.system.languagesProfileList(), + staleTime: Infinity, + }); } diff --git a/frontend/src/apis/hooks/movies.ts b/frontend/src/apis/hooks/movies.ts index ee8fe1100..6b1c5c2a5 100644 --- a/frontend/src/apis/hooks/movies.ts +++ b/frontend/src/apis/hooks/movies.ts @@ -1,12 +1,13 @@ +import { useEffect } from "react"; import { QueryClient, useMutation, useQuery, useQueryClient, -} from "react-query"; -import { usePaginationQuery } from "../queries/hooks"; -import { QueryKeys } from "../queries/keys"; -import api from "../raw"; +} from "@tanstack/react-query"; +import { usePaginationQuery } from "@/apis/queries/hooks"; +import { QueryKeys } from "@/apis/queries/keys"; +import api from "@/apis/raw"; const cacheMovies = (client: QueryClient, movies: Item.Movie[]) => { movies.forEach((item) => { @@ -14,33 +15,32 @@ const cacheMovies = (client: QueryClient, movies: Item.Movie[]) => { }); }; -export function useMoviesByIds(ids: number[]) { - const client = useQueryClient(); - return useQuery([QueryKeys.Movies, ...ids], () => api.movies.movies(ids), { - onSuccess: (data) => { - cacheMovies(client, data); - }, - }); -} - export function useMovieById(id: number) { - return useQuery([QueryKeys.Movies, id], async () => { - const response = await api.movies.movies([id]); - return response.length > 0 ? response[0] : undefined; + return useQuery({ + queryKey: [QueryKeys.Movies, id], + + queryFn: async () => { + const response = await api.movies.movies([id]); + return response.length > 0 ? response[0] : undefined; + }, }); } export function useMovies() { const client = useQueryClient(); - return useQuery( - [QueryKeys.Movies, QueryKeys.All], - () => api.movies.movies(), - { - onSuccess: (data) => { - cacheMovies(client, data); - }, - }, - ); + + const query = useQuery({ + queryKey: [QueryKeys.Movies, QueryKeys.All], + queryFn: () => api.movies.movies(), + }); + + useEffect(() => { + if (query.isSuccess && query.data) { + cacheMovies(client, query.data); + } + }, [query.isSuccess, query.data, client]); + + return query; } export function useMoviesPagination() { @@ -51,32 +51,37 @@ export function useMoviesPagination() { export function useMovieModification() { const client = useQueryClient(); - return useMutation( - [QueryKeys.Movies], - (form: FormType.ModifyItem) => api.movies.modify(form), - { - onSuccess: (_, form) => { - form.id.forEach((v) => { - client.invalidateQueries([QueryKeys.Movies, v]); + return useMutation({ + mutationKey: [QueryKeys.Movies], + mutationFn: (form: FormType.ModifyItem) => api.movies.modify(form), + + onSuccess: (_, form) => { + form.id.forEach((v) => { + void client.invalidateQueries({ + queryKey: [QueryKeys.Movies, v], }); - // TODO: query less - client.invalidateQueries([QueryKeys.Movies]); - }, + }); + + // TODO: query less + void client.invalidateQueries({ + queryKey: [QueryKeys.Movies], + }); }, - ); + }); } export function useMovieAction() { const client = useQueryClient(); - return useMutation( - [QueryKeys.Actions, QueryKeys.Movies], - (form: FormType.MoviesAction) => api.movies.action(form), - { - onSuccess: () => { - client.invalidateQueries([QueryKeys.Movies]); - }, + return useMutation({ + mutationKey: [QueryKeys.Actions, QueryKeys.Movies], + mutationFn: (form: FormType.MoviesAction) => api.movies.action(form), + + onSuccess: () => { + void client.invalidateQueries({ + queryKey: [QueryKeys.Movies], + }); }, - ); + }); } export function useMovieWantedPagination() { @@ -86,40 +91,49 @@ export function useMovieWantedPagination() { } export function useMovieBlacklist() { - return useQuery([QueryKeys.Movies, QueryKeys.Blacklist], () => - api.movies.blacklist(), - ); + return useQuery({ + queryKey: [QueryKeys.Movies, QueryKeys.Blacklist], + + queryFn: () => api.movies.blacklist(), + }); } export function useMovieAddBlacklist() { const client = useQueryClient(); - return useMutation( - [QueryKeys.Movies, QueryKeys.Blacklist], - (param: { id: number; form: FormType.AddBlacklist }) => { + return useMutation({ + mutationKey: [QueryKeys.Movies, QueryKeys.Blacklist], + + mutationFn: (param: { id: number; form: FormType.AddBlacklist }) => { const { id, form } = param; return api.movies.addBlacklist(id, form); }, - { - onSuccess: (_, { id }) => { - client.invalidateQueries([QueryKeys.Movies, QueryKeys.Blacklist]); - client.invalidateQueries([QueryKeys.Movies, id]); - }, + + onSuccess: (_, { id }) => { + void client.invalidateQueries({ + queryKey: [QueryKeys.Movies, QueryKeys.Blacklist], + }); + + void client.invalidateQueries({ + queryKey: [QueryKeys.Movies, id], + }); }, - ); + }); } export function useMovieDeleteBlacklist() { const client = useQueryClient(); - return useMutation( - [QueryKeys.Movies, QueryKeys.Blacklist], - (param: { all?: boolean; form?: FormType.DeleteBlacklist }) => + return useMutation({ + mutationKey: [QueryKeys.Movies, QueryKeys.Blacklist], + + mutationFn: (param: { all?: boolean; form?: FormType.DeleteBlacklist }) => api.movies.deleteBlacklist(param.all, param.form), - { - onSuccess: (_, param) => { - client.invalidateQueries([QueryKeys.Movies, QueryKeys.Blacklist]); - }, + + onSuccess: () => { + void client.invalidateQueries({ + queryKey: [QueryKeys.Movies, QueryKeys.Blacklist], + }); }, - ); + }); } export function useMovieHistoryPagination() { @@ -131,9 +145,15 @@ export function useMovieHistoryPagination() { } export function useMovieHistory(radarrId?: number) { - return useQuery([QueryKeys.Movies, QueryKeys.History, radarrId], () => { - if (radarrId) { - return api.movies.historyBy(radarrId); - } + return useQuery({ + queryKey: [QueryKeys.Movies, QueryKeys.History, radarrId], + + queryFn: () => { + if (radarrId) { + return api.movies.historyBy(radarrId); + } + + return []; + }, }); } diff --git a/frontend/src/apis/hooks/providers.ts b/frontend/src/apis/hooks/providers.ts index 5e4fed602..b40a24e2f 100644 --- a/frontend/src/apis/hooks/providers.ts +++ b/frontend/src/apis/hooks/providers.ts @@ -1,66 +1,82 @@ -import { useMutation, useQuery, useQueryClient } from "react-query"; -import { QueryKeys } from "../queries/keys"; -import api from "../raw"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { QueryKeys } from "@/apis/queries/keys"; +import api from "@/apis/raw"; export function useSystemProviders(history?: boolean) { - return useQuery( - [QueryKeys.System, QueryKeys.Providers, history ?? false], - () => api.providers.providers(history), - ); + return useQuery({ + queryKey: [QueryKeys.System, QueryKeys.Providers, history ?? false], + queryFn: () => api.providers.providers(history), + }); } export function useMoviesProvider(radarrId?: number) { - return useQuery( - [QueryKeys.System, QueryKeys.Providers, QueryKeys.Movies, radarrId], - () => { + return useQuery({ + queryKey: [ + QueryKeys.System, + QueryKeys.Providers, + QueryKeys.Movies, + radarrId, + ], + + queryFn: () => { if (radarrId) { return api.providers.movies(radarrId); } + + return []; }, - { - staleTime: 0, - }, - ); + + staleTime: 0, + }); } export function useEpisodesProvider(episodeId?: number) { - return useQuery( - [QueryKeys.System, QueryKeys.Providers, QueryKeys.Episodes, episodeId], - () => { + return useQuery({ + queryKey: [ + QueryKeys.System, + QueryKeys.Providers, + QueryKeys.Episodes, + episodeId, + ], + + queryFn: () => { if (episodeId) { return api.providers.episodes(episodeId); } + + return []; }, - { - staleTime: 0, - }, - ); + + staleTime: 0, + }); } export function useResetProvider() { const client = useQueryClient(); - return useMutation( - [QueryKeys.System, QueryKeys.Providers], - () => api.providers.reset(), - { - onSuccess: () => { - client.invalidateQueries([QueryKeys.System, QueryKeys.Providers]); - }, + return useMutation({ + mutationKey: [QueryKeys.System, QueryKeys.Providers], + mutationFn: () => api.providers.reset(), + + onSuccess: () => { + client.invalidateQueries({ + queryKey: [QueryKeys.System, QueryKeys.Providers], + }); }, - ); + }); } export function useDownloadEpisodeSubtitles() { const client = useQueryClient(); - return useMutation( - [ + return useMutation({ + mutationKey: [ QueryKeys.System, QueryKeys.Providers, QueryKeys.Subtitles, QueryKeys.Episodes, ], - (param: { + + mutationFn: (param: { seriesId: number; episodeId: number; form: FormType.ManualDownload; @@ -70,30 +86,33 @@ export function useDownloadEpisodeSubtitles() { param.episodeId, param.form, ), - { - onSuccess: (_, param) => { - client.invalidateQueries([QueryKeys.Series, param.seriesId]); - }, + + onSuccess: (_, param) => { + client.invalidateQueries({ + queryKey: [QueryKeys.Series, param.seriesId], + }); }, - ); + }); } export function useDownloadMovieSubtitles() { const client = useQueryClient(); - return useMutation( - [ + return useMutation({ + mutationKey: [ QueryKeys.System, QueryKeys.Providers, QueryKeys.Subtitles, QueryKeys.Movies, ], - (param: { radarrId: number; form: FormType.ManualDownload }) => + + mutationFn: (param: { radarrId: number; form: FormType.ManualDownload }) => api.providers.downloadMovieSubtitle(param.radarrId, param.form), - { - onSuccess: (_, param) => { - client.invalidateQueries([QueryKeys.Movies, param.radarrId]); - }, + + onSuccess: (_, param) => { + client.invalidateQueries({ + queryKey: [QueryKeys.Movies, param.radarrId], + }); }, - ); + }); } diff --git a/frontend/src/apis/hooks/series.ts b/frontend/src/apis/hooks/series.ts index 1e395a6e5..2add91194 100644 --- a/frontend/src/apis/hooks/series.ts +++ b/frontend/src/apis/hooks/series.ts @@ -1,12 +1,13 @@ +import { useEffect } from "react"; import { QueryClient, useMutation, useQuery, useQueryClient, -} from "react-query"; -import { usePaginationQuery } from "../queries/hooks"; -import { QueryKeys } from "../queries/keys"; -import api from "../raw"; +} from "@tanstack/react-query"; +import { usePaginationQuery } from "@/apis/queries/hooks"; +import { QueryKeys } from "@/apis/queries/keys"; +import api from "@/apis/raw"; function cacheSeries(client: QueryClient, series: Item.Series[]) { series.forEach((item) => { @@ -16,31 +17,47 @@ function cacheSeries(client: QueryClient, series: Item.Series[]) { export function useSeriesByIds(ids: number[]) { const client = useQueryClient(); - return useQuery([QueryKeys.Series, ...ids], () => api.series.series(ids), { - onSuccess: (data) => { - cacheSeries(client, data); - }, + + const query = useQuery({ + queryKey: [QueryKeys.Series, ...ids], + queryFn: () => api.series.series(ids), }); + + useEffect(() => { + if (query.isSuccess && query.data) { + cacheSeries(client, query.data); + } + }, [query.isSuccess, query.data, client]); + + return query; } export function useSeriesById(id: number) { - return useQuery([QueryKeys.Series, id], async () => { - const response = await api.series.series([id]); - return response.length > 0 ? response[0] : undefined; + return useQuery({ + queryKey: [QueryKeys.Series, id], + + queryFn: async () => { + const response = await api.series.series([id]); + return response.length > 0 ? response[0] : undefined; + }, }); } export function useSeries() { const client = useQueryClient(); - return useQuery( - [QueryKeys.Series, QueryKeys.All], - () => api.series.series(), - { - onSuccess: (data) => { - cacheSeries(client, data); - }, - }, - ); + + const query = useQuery({ + queryKey: [QueryKeys.Series, QueryKeys.All], + queryFn: () => api.series.series(), + }); + + useEffect(() => { + if (query.isSuccess && query.data) { + cacheSeries(client, query.data); + } + }, [query.isSuccess, query.data, client]); + + return query; } export function useSeriesPagination() { @@ -51,29 +68,33 @@ export function useSeriesPagination() { export function useSeriesModification() { const client = useQueryClient(); - return useMutation( - [QueryKeys.Series], - (form: FormType.ModifyItem) => api.series.modify(form), - { - onSuccess: (_, form) => { - form.id.forEach((v) => { - client.invalidateQueries([QueryKeys.Series, v]); + return useMutation({ + mutationKey: [QueryKeys.Series], + mutationFn: (form: FormType.ModifyItem) => api.series.modify(form), + + onSuccess: (_, form) => { + form.id.forEach((v) => { + client.invalidateQueries({ + queryKey: [QueryKeys.Series, v], }); - client.invalidateQueries([QueryKeys.Series]); - }, + }); + client.invalidateQueries({ + queryKey: [QueryKeys.Series], + }); }, - ); + }); } export function useSeriesAction() { const client = useQueryClient(); - return useMutation( - [QueryKeys.Actions, QueryKeys.Series], - (form: FormType.SeriesAction) => api.series.action(form), - { - onSuccess: () => { - client.invalidateQueries([QueryKeys.Series]); - }, + return useMutation({ + mutationKey: [QueryKeys.Actions, QueryKeys.Series], + mutationFn: (form: FormType.SeriesAction) => api.series.action(form), + + onSuccess: () => { + client.invalidateQueries({ + queryKey: [QueryKeys.Series], + }); }, - ); + }); } diff --git a/frontend/src/apis/hooks/status.ts b/frontend/src/apis/hooks/status.ts index 46a73cfda..66af943fc 100644 --- a/frontend/src/apis/hooks/status.ts +++ b/frontend/src/apis/hooks/status.ts @@ -1,16 +1,28 @@ -import { useIsMutating } from "react-query"; -import { QueryKeys } from "../queries/keys"; +import { useIsMutating } from "@tanstack/react-query"; +import { QueryKeys } from "@/apis/queries/keys"; export function useIsAnyActionRunning() { - return useIsMutating([QueryKeys.Actions]) > 0; + return ( + useIsMutating({ + mutationKey: [QueryKeys.Actions], + }) > 0 + ); } export function useIsMovieActionRunning() { - return useIsMutating([QueryKeys.Actions, QueryKeys.Movies]) > 0; + return ( + useIsMutating({ + mutationKey: [QueryKeys.Actions, QueryKeys.Movies], + }) > 0 + ); } export function useIsSeriesActionRunning() { - return useIsMutating([QueryKeys.Actions, QueryKeys.Series]) > 0; + return ( + useIsMutating({ + mutationKey: [QueryKeys.Actions, QueryKeys.Series], + }) > 0 + ); } export function useIsAnyMutationRunning() { diff --git a/frontend/src/apis/hooks/subtitles.ts b/frontend/src/apis/hooks/subtitles.ts index 7864cdcbb..dfc263ff6 100644 --- a/frontend/src/apis/hooks/subtitles.ts +++ b/frontend/src/apis/hooks/subtitles.ts @@ -1,6 +1,6 @@ -import { useMutation, useQuery, useQueryClient } from "react-query"; -import { QueryKeys } from "../queries/keys"; -import api from "../raw"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { QueryKeys } from "@/apis/queries/keys"; +import api from "@/apis/raw"; export function useSubtitleAction() { const client = useQueryClient(); @@ -8,23 +8,29 @@ export function useSubtitleAction() { action: string; form: FormType.ModifySubtitle; } - return useMutation( - [QueryKeys.Subtitles], - (param: Param) => api.subtitles.modify(param.action, param.form), - { - onSuccess: (_, param) => { - client.invalidateQueries([QueryKeys.History]); - - // TODO: Query less - const { type, id } = param.form; - if (type === "episode") { - client.invalidateQueries([QueryKeys.Series, id]); - } else { - client.invalidateQueries([QueryKeys.Movies, id]); - } - }, + return useMutation({ + mutationKey: [QueryKeys.Subtitles], + mutationFn: (param: Param) => + api.subtitles.modify(param.action, param.form), + + onSuccess: (_, param) => { + client.invalidateQueries({ + queryKey: [QueryKeys.History], + }); + + // TODO: Query less + const { type, id } = param.form; + if (type === "episode") { + client.invalidateQueries({ + queryKey: [QueryKeys.Series, id], + }); + } else { + client.invalidateQueries({ + queryKey: [QueryKeys.Movies, id], + }); + } }, - ); + }); } export function useEpisodeSubtitleModification() { @@ -36,42 +42,48 @@ export function useEpisodeSubtitleModification() { form: T; } - const download = useMutation( - [QueryKeys.Subtitles, QueryKeys.Episodes], - (param: Param) => + const download = useMutation({ + mutationKey: [QueryKeys.Subtitles, QueryKeys.Episodes], + + mutationFn: (param: Param) => api.episodes.downloadSubtitles( param.seriesId, param.episodeId, param.form, ), - { - onSuccess: (_, param) => { - client.invalidateQueries([QueryKeys.Series, param.seriesId]); - }, + + onSuccess: (_, param) => { + client.invalidateQueries({ + queryKey: [QueryKeys.Series, param.seriesId], + }); }, - ); + }); - const remove = useMutation( - [QueryKeys.Subtitles, QueryKeys.Episodes], - (param: Param) => + const remove = useMutation({ + mutationKey: [QueryKeys.Subtitles, QueryKeys.Episodes], + + mutationFn: (param: Param) => api.episodes.deleteSubtitles(param.seriesId, param.episodeId, param.form), - { - onSuccess: (_, param) => { - client.invalidateQueries([QueryKeys.Series, param.seriesId]); - }, + + onSuccess: (_, param) => { + client.invalidateQueries({ + queryKey: [QueryKeys.Series, param.seriesId], + }); }, - ); + }); - const upload = useMutation( - [QueryKeys.Subtitles, QueryKeys.Episodes], - (param: Param) => + const upload = useMutation({ + mutationKey: [QueryKeys.Subtitles, QueryKeys.Episodes], + + mutationFn: (param: Param) => api.episodes.uploadSubtitles(param.seriesId, param.episodeId, param.form), - { - onSuccess: (_, { seriesId }) => { - client.invalidateQueries([QueryKeys.Series, seriesId]); - }, + + onSuccess: (_, { seriesId }) => { + client.invalidateQueries({ + queryKey: [QueryKeys.Series, seriesId], + }); }, - ); + }); return { download, remove, upload }; } @@ -84,46 +96,54 @@ export function useMovieSubtitleModification() { form: T; } - const download = useMutation( - [QueryKeys.Subtitles, QueryKeys.Movies], - (param: Param) => + const download = useMutation({ + mutationKey: [QueryKeys.Subtitles, QueryKeys.Movies], + + mutationFn: (param: Param) => api.movies.downloadSubtitles(param.radarrId, param.form), - { - onSuccess: (_, param) => { - client.invalidateQueries([QueryKeys.Movies, param.radarrId]); - }, + + onSuccess: (_, param) => { + client.invalidateQueries({ + queryKey: [QueryKeys.Movies, param.radarrId], + }); }, - ); + }); + + const remove = useMutation({ + mutationKey: [QueryKeys.Subtitles, QueryKeys.Movies], - const remove = useMutation( - [QueryKeys.Subtitles, QueryKeys.Movies], - (param: Param) => + mutationFn: (param: Param) => api.movies.deleteSubtitles(param.radarrId, param.form), - { - onSuccess: (_, param) => { - client.invalidateQueries([QueryKeys.Movies, param.radarrId]); - }, + + onSuccess: (_, param) => { + client.invalidateQueries({ + queryKey: [QueryKeys.Movies, param.radarrId], + }); }, - ); + }); - const upload = useMutation( - [QueryKeys.Subtitles, QueryKeys.Movies], - (param: Param) => + const upload = useMutation({ + mutationKey: [QueryKeys.Subtitles, QueryKeys.Movies], + + mutationFn: (param: Param) => api.movies.uploadSubtitles(param.radarrId, param.form), - { - onSuccess: (_, { radarrId }) => { - client.invalidateQueries([QueryKeys.Movies, radarrId]); - }, + + onSuccess: (_, { radarrId }) => { + client.invalidateQueries({ + queryKey: [QueryKeys.Movies, radarrId], + }); }, - ); + }); return { download, remove, upload }; } export function useSubtitleInfos(names: string[]) { - return useQuery([QueryKeys.Subtitles, QueryKeys.Infos, names], () => - api.subtitles.info(names), - ); + return useQuery({ + queryKey: [QueryKeys.Subtitles, QueryKeys.Infos, names], + + queryFn: () => api.subtitles.info(names), + }); } export function useRefTracksByEpisodeId( @@ -131,11 +151,17 @@ export function useRefTracksByEpisodeId( sonarrEpisodeId: number, isEpisode: boolean, ) { - return useQuery( - [QueryKeys.Episodes, sonarrEpisodeId, QueryKeys.Subtitles, subtitlesPath], - () => api.subtitles.getRefTracksByEpisodeId(subtitlesPath, sonarrEpisodeId), - { enabled: isEpisode }, - ); + return useQuery({ + queryKey: [ + QueryKeys.Episodes, + sonarrEpisodeId, + QueryKeys.Subtitles, + subtitlesPath, + ], + queryFn: () => + api.subtitles.getRefTracksByEpisodeId(subtitlesPath, sonarrEpisodeId), + enabled: isEpisode, + }); } export function useRefTracksByMovieId( @@ -143,9 +169,15 @@ export function useRefTracksByMovieId( radarrMovieId: number, isMovie: boolean, ) { - return useQuery( - [QueryKeys.Movies, radarrMovieId, QueryKeys.Subtitles, subtitlesPath], - () => api.subtitles.getRefTracksByMovieId(subtitlesPath, radarrMovieId), - { enabled: isMovie }, - ); + return useQuery({ + queryKey: [ + QueryKeys.Movies, + radarrMovieId, + QueryKeys.Subtitles, + subtitlesPath, + ], + queryFn: () => + api.subtitles.getRefTracksByMovieId(subtitlesPath, radarrMovieId), + enabled: isMovie, + }); } diff --git a/frontend/src/apis/hooks/system.ts b/frontend/src/apis/hooks/system.ts index 26946e910..a0ce17fb9 100644 --- a/frontend/src/apis/hooks/system.ts +++ b/frontend/src/apis/hooks/system.ts @@ -1,20 +1,18 @@ +import { useMemo } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { QueryKeys } from "@/apis/queries/keys"; +import api from "@/apis/raw"; import { Environment } from "@/utilities"; import { setAuthenticated } from "@/utilities/event"; -import { useMemo } from "react"; -import { useMutation, useQuery, useQueryClient } from "react-query"; -import { QueryKeys } from "../queries/keys"; -import api from "../raw"; export function useBadges() { - return useQuery( - [QueryKeys.System, QueryKeys.Badges], - () => api.badges.all(), - { - refetchOnWindowFocus: "always", - refetchInterval: 1000 * 60, - staleTime: 1000 * 10, - }, - ); + return useQuery({ + queryKey: [QueryKeys.System, QueryKeys.Badges], + queryFn: () => api.badges.all(), + refetchOnWindowFocus: "always", + refetchInterval: 1000 * 60, + staleTime: 1000 * 10, + }); } export function useFileSystem( @@ -22,9 +20,10 @@ export function useFileSystem( path: string, enabled: boolean, ) { - return useQuery( - [QueryKeys.FileSystem, type, path], - () => { + return useQuery({ + queryKey: [QueryKeys.FileSystem, type, path], + + queryFn: () => { if (type === "bazarr") { return api.files.bazarr(path); } else if (type === "radarr") { @@ -32,53 +31,68 @@ export function useFileSystem( } else if (type === "sonarr") { return api.files.sonarr(path); } + + return []; }, - { - enabled, - }, - ); + + enabled, + }); } export function useSystemSettings() { - return useQuery( - [QueryKeys.System, QueryKeys.Settings], - () => api.system.settings(), - { - staleTime: Infinity, - }, - ); + return useQuery({ + queryKey: [QueryKeys.System, QueryKeys.Settings], + queryFn: () => api.system.settings(), + staleTime: Infinity, + }); } export function useSettingsMutation() { const client = useQueryClient(); - return useMutation( - [QueryKeys.System, QueryKeys.Settings], - (data: LooseObject) => api.system.updateSettings(data), - { - onSuccess: () => { - client.invalidateQueries([QueryKeys.System]); - client.invalidateQueries([QueryKeys.Series]); - client.invalidateQueries([QueryKeys.Episodes]); - client.invalidateQueries([QueryKeys.Movies]); - client.invalidateQueries([QueryKeys.Wanted]); - client.invalidateQueries([QueryKeys.Badges]); - }, + return useMutation({ + mutationKey: [QueryKeys.System, QueryKeys.Settings], + mutationFn: (data: LooseObject) => api.system.updateSettings(data), + + onSuccess: () => { + void client.invalidateQueries({ + queryKey: [QueryKeys.System], + }); + + void client.invalidateQueries({ + queryKey: [QueryKeys.Series], + }); + + void client.invalidateQueries({ + queryKey: [QueryKeys.Episodes], + }); + + void client.invalidateQueries({ + queryKey: [QueryKeys.Movies], + }); + + void client.invalidateQueries({ + queryKey: [QueryKeys.Wanted], + }); + + void client.invalidateQueries({ + queryKey: [QueryKeys.Badges], + }); }, - ); + }); } export function useServerSearch(query: string, enabled: boolean) { - return useQuery( - [QueryKeys.System, QueryKeys.Search, query], - () => api.system.search(query), - { - enabled, - }, - ); + return useQuery({ + queryKey: [QueryKeys.System, QueryKeys.Search, query], + queryFn: () => api.system.search(query), + enabled, + }); } export function useSystemLogs() { - return useQuery([QueryKeys.System, QueryKeys.Logs], () => api.system.logs(), { + return useQuery({ + queryKey: [QueryKeys.System, QueryKeys.Logs], + queryFn: () => api.system.logs(), refetchOnWindowFocus: "always", refetchInterval: 1000 * 60, staleTime: 1000 * 10, @@ -87,171 +101,189 @@ export function useSystemLogs() { export function useDeleteLogs() { const client = useQueryClient(); - return useMutation( - [QueryKeys.System, QueryKeys.Logs], - () => api.system.deleteLogs(), - { - onSuccess: () => { - client.invalidateQueries([QueryKeys.System, QueryKeys.Logs]); - }, + return useMutation({ + mutationKey: [QueryKeys.System, QueryKeys.Logs], + mutationFn: () => api.system.deleteLogs(), + + onSuccess: () => { + void client.invalidateQueries({ + queryKey: [QueryKeys.System, QueryKeys.Logs], + }); }, - ); + }); } export function useSystemAnnouncements() { - return useQuery( - [QueryKeys.System, QueryKeys.Announcements], - () => api.system.announcements(), - { - refetchOnWindowFocus: "always", - refetchInterval: 1000 * 60, - staleTime: 1000 * 10, - }, - ); + return useQuery({ + queryKey: [QueryKeys.System, QueryKeys.Announcements], + queryFn: () => api.system.announcements(), + refetchOnWindowFocus: "always", + refetchInterval: 1000 * 60, + staleTime: 1000 * 10, + }); } export function useSystemAnnouncementsAddDismiss() { const client = useQueryClient(); - return useMutation( - [QueryKeys.System, QueryKeys.Announcements], - (param: { hash: string }) => { + return useMutation({ + mutationKey: [QueryKeys.System, QueryKeys.Announcements], + + mutationFn: (param: { hash: string }) => { const { hash } = param; return api.system.addAnnouncementsDismiss(hash); }, - { - onSuccess: (_, { hash }) => { - client.invalidateQueries([QueryKeys.System, QueryKeys.Announcements]); - client.invalidateQueries([QueryKeys.System, QueryKeys.Badges]); - }, + + onSuccess: () => { + void client.invalidateQueries({ + queryKey: [QueryKeys.System, QueryKeys.Announcements], + }); + + void client.invalidateQueries({ + queryKey: [QueryKeys.System, QueryKeys.Badges], + }); }, - ); + }); } export function useSystemTasks() { - return useQuery( - [QueryKeys.System, QueryKeys.Tasks], - () => api.system.tasks(), - { - refetchOnWindowFocus: "always", - refetchInterval: 1000 * 60, - staleTime: 1000 * 10, - }, - ); + return useQuery({ + queryKey: [QueryKeys.System, QueryKeys.Tasks], + queryFn: () => api.system.tasks(), + refetchOnWindowFocus: "always", + refetchInterval: 1000 * 60, + staleTime: 1000 * 10, + }); } export function useRunTask() { const client = useQueryClient(); - return useMutation( - [QueryKeys.System, QueryKeys.Tasks], - (id: string) => api.system.runTask(id), - { - onSuccess: () => { - client.invalidateQueries([QueryKeys.System, QueryKeys.Tasks]); - client.invalidateQueries([QueryKeys.System, QueryKeys.Backups]); - }, + return useMutation({ + mutationKey: [QueryKeys.System, QueryKeys.Tasks], + mutationFn: (id: string) => api.system.runTask(id), + + onSuccess: () => { + void client.invalidateQueries({ + queryKey: [QueryKeys.System, QueryKeys.Tasks], + }); + + void client.invalidateQueries({ + queryKey: [QueryKeys.System, QueryKeys.Backups], + }); }, - ); + }); } export function useSystemBackups() { - return useQuery([QueryKeys.System, "backups"], () => api.system.backups()); + return useQuery({ + queryKey: [QueryKeys.System, "backups"], + queryFn: () => api.system.backups(), + }); } export function useCreateBackups() { const client = useQueryClient(); - return useMutation( - [QueryKeys.System, QueryKeys.Backups], - () => api.system.createBackups(), - { - onSuccess: () => { - client.invalidateQueries([QueryKeys.System, QueryKeys.Backups]); - }, + return useMutation({ + mutationKey: [QueryKeys.System, QueryKeys.Backups], + mutationFn: () => api.system.createBackups(), + + onSuccess: () => { + void client.invalidateQueries({ + queryKey: [QueryKeys.System, QueryKeys.Backups], + }); }, - ); + }); } export function useRestoreBackups() { const client = useQueryClient(); - return useMutation( - [QueryKeys.System, QueryKeys.Backups], - (filename: string) => api.system.restoreBackups(filename), - { - onSuccess: () => { - client.invalidateQueries([QueryKeys.System, QueryKeys.Backups]); - }, + return useMutation({ + mutationKey: [QueryKeys.System, QueryKeys.Backups], + mutationFn: (filename: string) => api.system.restoreBackups(filename), + + onSuccess: () => { + void client.invalidateQueries({ + queryKey: [QueryKeys.System, QueryKeys.Backups], + }); }, - ); + }); } export function useDeleteBackups() { const client = useQueryClient(); - return useMutation( - [QueryKeys.System, QueryKeys.Backups], - (filename: string) => api.system.deleteBackups(filename), - { - onSuccess: () => { - client.invalidateQueries([QueryKeys.System, QueryKeys.Backups]); - }, + return useMutation({ + mutationKey: [QueryKeys.System, QueryKeys.Backups], + mutationFn: (filename: string) => api.system.deleteBackups(filename), + + onSuccess: () => { + void client.invalidateQueries({ + queryKey: [QueryKeys.System, QueryKeys.Backups], + }); }, - ); + }); } export function useSystemStatus() { - return useQuery([QueryKeys.System, "status"], () => api.system.status()); + return useQuery({ + queryKey: [QueryKeys.System, "status"], + queryFn: () => api.system.status(), + }); } export function useSystemHealth() { - return useQuery([QueryKeys.System, "health"], () => api.system.health()); + return useQuery({ + queryKey: [QueryKeys.System, "health"], + queryFn: () => api.system.health(), + }); } export function useSystemReleases() { - return useQuery([QueryKeys.System, "releases"], () => api.system.releases()); + return useQuery({ + queryKey: [QueryKeys.System, "releases"], + queryFn: () => api.system.releases(), + }); } export function useSystem() { const client = useQueryClient(); - const { mutate: logout, isLoading: isLoggingOut } = useMutation( - [QueryKeys.System, QueryKeys.Actions], - () => api.system.logout(), - { - onSuccess: () => { - setAuthenticated(false); - client.clear(); - }, + const { mutate: logout, isPending: isLoggingOut } = useMutation({ + mutationKey: [QueryKeys.System, QueryKeys.Actions], + mutationFn: () => api.system.logout(), + + onSuccess: () => { + setAuthenticated(false); + client.clear(); }, - ); + }); + + const { mutate: login, isPending: isLoggingIn } = useMutation({ + mutationKey: [QueryKeys.System, QueryKeys.Actions], - const { mutate: login, isLoading: isLoggingIn } = useMutation( - [QueryKeys.System, QueryKeys.Actions], - (param: { username: string; password: string }) => + mutationFn: (param: { username: string; password: string }) => api.system.login(param.username, param.password), - { - onSuccess: () => { - // TODO: Hard-coded value - window.location.replace(Environment.baseUrl); - }, + + onSuccess: () => { + // TODO: Hard-coded value + window.location.replace(Environment.baseUrl); }, - ); + }); - const { mutate: shutdown, isLoading: isShuttingDown } = useMutation( - [QueryKeys.System, QueryKeys.Actions], - () => api.system.shutdown(), - { - onSuccess: () => { - client.clear(); - }, + const { mutate: shutdown, isPending: isShuttingDown } = useMutation({ + mutationKey: [QueryKeys.System, QueryKeys.Actions], + mutationFn: () => api.system.shutdown(), + + onSuccess: () => { + client.clear(); }, - ); + }); + + const { mutate: restart, isPending: isRestarting } = useMutation({ + mutationKey: [QueryKeys.System, QueryKeys.Actions], + mutationFn: () => api.system.restart(), - const { mutate: restart, isLoading: isRestarting } = useMutation( - [QueryKeys.System, QueryKeys.Actions], - () => api.system.restart(), - { - onSuccess: () => { - client.clear(); - }, + onSuccess: () => { + client.clear(); }, - ); + }); return useMemo( () => ({ diff --git a/frontend/src/apis/queries/hooks.ts b/frontend/src/apis/queries/hooks.ts index 5f9bf63d3..2f17b5efe 100644 --- a/frontend/src/apis/queries/hooks.ts +++ b/frontend/src/apis/queries/hooks.ts @@ -1,12 +1,12 @@ -import { GetItemId, useOnValueChange } from "@/utilities"; -import { usePageSize } from "@/utilities/storage"; import { useCallback, useEffect, useState } from "react"; import { QueryKey, - UseQueryResult, useQuery, useQueryClient, -} from "react-query"; + UseQueryResult, +} from "@tanstack/react-query"; +import { GetItemId, useOnValueChange } from "@/utilities"; +import { usePageSize } from "@/utilities/storage"; import { QueryKeys } from "./keys"; export type UsePaginationQueryResult = UseQueryResult< @@ -39,31 +39,31 @@ export function usePaginationQuery< const start = page * pageSize; - const results = useQuery( - [...queryKey, QueryKeys.Range, { start, size: pageSize }], - () => { + const results = useQuery({ + queryKey: [...queryKey, QueryKeys.Range, { start, size: pageSize }], + + queryFn: () => { const param: Parameter.Range = { start, length: pageSize, }; return queryFn(param); }, - { - onSuccess: ({ data }) => { - if (cacheIndividual) { - data.forEach((item) => { - const id = GetItemId(item); - if (id) { - client.setQueryData([...queryKey, id], item); - } - }); - } - }, - }, - ); + }); const { data } = results; + useEffect(() => { + if (results.isSuccess && results.data && cacheIndividual) { + results.data.data.forEach((item) => { + const id = GetItemId(item); + if (id) { + client.setQueryData([...queryKey, id], item); + } + }); + } + }, [results.isSuccess, results.data, client, cacheIndividual, queryKey]); + const totalCount = data?.total ?? 0; const pageCount = Math.ceil(totalCount / pageSize); diff --git a/frontend/src/apis/queries/index.ts b/frontend/src/apis/queries/index.ts index a1a17ffd9..75958f111 100644 --- a/frontend/src/apis/queries/index.ts +++ b/frontend/src/apis/queries/index.ts @@ -1,4 +1,4 @@ -import { QueryClient } from "react-query"; +import { QueryClient } from "@tanstack/react-query"; const queryClient = new QueryClient({ defaultOptions: { @@ -6,7 +6,11 @@ const queryClient = new QueryClient({ refetchOnWindowFocus: false, retry: false, staleTime: 1000 * 60, - keepPreviousData: true, + networkMode: "offlineFirst", + placeholderData: (previousData: object) => previousData, + }, + mutations: { + networkMode: "offlineFirst", }, }, }); diff --git a/frontend/src/apis/raw/client.ts b/frontend/src/apis/raw/client.ts index d77b81205..f3c2f53a3 100644 --- a/frontend/src/apis/raw/client.ts +++ b/frontend/src/apis/raw/client.ts @@ -1,10 +1,10 @@ +import { showNotification } from "@mantine/notifications"; +import Axios, { AxiosError, AxiosInstance, CancelTokenSource } from "axios"; import socketio from "@/modules/socketio"; import { notification } from "@/modules/task"; +import { Environment } from "@/utilities"; import { LOG } from "@/utilities/console"; import { setAuthenticated } from "@/utilities/event"; -import { showNotification } from "@mantine/notifications"; -import Axios, { AxiosError, AxiosInstance, CancelTokenSource } from "axios"; -import { Environment } from "../../utilities"; function GetErrorMessage(data: unknown, defaultMsg = "Unknown error"): string { if (typeof data === "string") { diff --git a/frontend/src/assets/_bazarr.scss b/frontend/src/assets/_bazarr.scss new file mode 100644 index 000000000..96e44e619 --- /dev/null +++ b/frontend/src/assets/_bazarr.scss @@ -0,0 +1,82 @@ +$color-brand-0: #f8f0fc; +$color-brand-1: #f3d9fa; +$color-brand-2: #eebefa; +$color-brand-3: #e599f7; +$color-brand-4: #da77f2; +$color-brand-5: #cc5de8; +$color-brand-6: #be4bdb; +$color-brand-7: #ae3ec9; +$color-brand-8: #9c36b5; +$color-brand-9: #862e9c; + +// Based on Mantine Cyan +$color-highlight-0: #e3fafc; +$color-highlight-1: #c5f6fa; +$color-highlight-2: #99e9f2; +$color-highlight-3: #66d9e8; +$color-highlight-4: #3bc9db; +$color-highlight-5: #22b8cf; +$color-highlight-6: #15aabf; +$color-highlight-7: #1098ad; +$color-highlight-8: #0c8599; +$color-highlight-9: #0b7285; + +// Based on Mantine Yellow +$color-warning-0: #fff9db; +$color-warning-1: #fff3bf; +$color-warning-2: #ffec99; +$color-warning-3: #ffe066; +$color-warning-4: #ffd43b; +$color-warning-5: #fcc419; +$color-warning-6: #fab005; +$color-warning-7: #f59f00; +$color-warning-8: #f08c00; +$color-warning-9: #e67700; + +// Based on Mantine Gray +$color-disabled-0: #f8f9fa; +$color-disabled-1: #f1f3f5; +$color-disabled-2: #e9ecef; +$color-disabled-3: #dee2e6; +$color-disabled-4: #ced4da; +$color-disabled-5: #adb5bd; +$color-disabled-6: #868e96; +$color-disabled-7: #495057; +$color-disabled-8: #343a40; +$color-disabled-9: #212529; + +$header-height: 64px; + +:global { + .table-long-break { + overflow-wrap: anywhere; + } + + .table-primary { + display: inline-block; + + font-size: var(--mantine-font-size-sm); + + @include smaller-than($mantine-breakpoint-sm) { + min-width: 12rem; + } + } + + .table-no-wrap { + white-space: nowrap; + } + + .table-select { + display: inline-block; + + @include smaller-than($mantine-breakpoint-sm) { + min-width: 10rem; + } + } +} + +:root { + @include dark { + --mantine-color-body: var(--mantine-color-dark-8); + } +} diff --git a/frontend/src/assets/_mantine.scss b/frontend/src/assets/_mantine.scss new file mode 100644 index 000000000..93412c636 --- /dev/null +++ b/frontend/src/assets/_mantine.scss @@ -0,0 +1,61 @@ +@use "sass:math"; + +$mantine-breakpoint-xs: "36em"; +$mantine-breakpoint-sm: "48em"; +$mantine-breakpoint-md: "62em"; +$mantine-breakpoint-lg: "75em"; +$mantine-breakpoint-xl: "88em"; + +@function rem($value) { + @return #{math.div(math.div($value, $value * 0 + 1), 16)}rem; +} + +@mixin light { + [data-mantine-color-scheme="light"] & { + @content; + } +} + +@mixin dark { + [data-mantine-color-scheme="dark"] & { + @content; + } +} + +@mixin hover { + @media (hover: hover) { + &:hover { + @content; + } + } + + @media (hover: none) { + &:active { + @content; + } + } +} + +@mixin smaller-than($breakpoint) { + @media (max-width: $breakpoint) { + @content; + } +} + +@mixin larger-than($breakpoint) { + @media (min-width: $breakpoint) { + @content; + } +} + +@mixin rtl { + [dir="rtl"] & { + @content; + } +} + +@mixin ltr { + [dir="ltr"] & { + @content; + } +} diff --git a/frontend/src/assets/_variables.module.scss b/frontend/src/assets/_variables.module.scss new file mode 100644 index 000000000..262d285b2 --- /dev/null +++ b/frontend/src/assets/_variables.module.scss @@ -0,0 +1,18 @@ +$navbar-width: 200; + +:export { + colorBrand0: $color-brand-0; + colorBrand1: $color-brand-1; + colorBrand2: $color-brand-2; + colorBrand3: $color-brand-3; + colorBrand4: $color-brand-4; + colorBrand5: $color-brand-5; + colorBrand6: $color-brand-6; + colorBrand7: $color-brand-7; + colorBrand8: $color-brand-8; + colorBrand9: $color-brand-9; + + headerHeight: $header-height; + + navBarWidth: $navbar-width; +} diff --git a/frontend/src/assets/action_icon.module.scss b/frontend/src/assets/action_icon.module.scss new file mode 100644 index 000000000..d23cb6ce2 --- /dev/null +++ b/frontend/src/assets/action_icon.module.scss @@ -0,0 +1,16 @@ +@layer mantine { + .root { + --ai-bg: transparent; + + @include light { + color: var(--mantine-color-dark-2); + --ai-hover: var(--mantine-color-gray-1); + --ai-hover-color: var(--mantine-color-gray-1); + } + + @include dark { + color: var(--mantine-color-dark-0); + --ai-hover: var(--mantine-color-gray-8); + } + } +} diff --git a/frontend/src/assets/badge.module.scss b/frontend/src/assets/badge.module.scss new file mode 100644 index 000000000..5b31be59e --- /dev/null +++ b/frontend/src/assets/badge.module.scss @@ -0,0 +1,54 @@ +@layer mantine { + .root { + background-color: transparentize($color-brand-6, 0.8); + + &[data-variant="warning"] { + color: lighten($color-warning-2, 0.8); + background-color: transparentize($color-warning-6, 0.8); + } + + &[data-variant="highlight"] { + color: lighten($color-highlight-2, 1); + background-color: transparentize($color-highlight-5, 0.8); + } + + &[data-variant="disabled"] { + color: lighten($color-disabled-0, 1); + background-color: transparentize($color-disabled-7, 0.8); + } + + &[data-variant="light"] { + color: var(--mantine-color-dark-0); + background-color: transparentize($color-disabled-9, 0.8); + } + + @include light { + color: $color-brand-6; + background-color: transparentize($color-brand-3, 0.8); + + &[data-variant="warning"] { + color: darken($color-warning-7, 1); + background-color: transparentize($color-warning-6, 0.8); + } + + &[data-variant="disabled"] { + color: darken($color-disabled-6, 1); + background-color: transparentize($color-disabled-4, 0.8); + } + + &[data-variant="highlight"] { + color: darken($color-highlight-6, 1); + background-color: transparentize($color-highlight-5, 0.8); + } + + &[data-variant="light"] { + color: var(--mantine-color-black); + background-color: var(--mantine-color-gray-5); + } + } + } + + .label { + overflow: visible; + } +} diff --git a/frontend/src/assets/button.module.scss b/frontend/src/assets/button.module.scss new file mode 100644 index 000000000..0c466a4c4 --- /dev/null +++ b/frontend/src/assets/button.module.scss @@ -0,0 +1,18 @@ +@layer mantine { + .root { + @include dark { + color: var(--mantine-color-dark-0); + } + + &[data-variant="danger"] { + background-color: var(--mantine-color-red-9); + color: var(--mantine-color-red-0); + } + } + + .root:disabled { + @include dark { + color: var(--mantine-color-dark-9); + } + } +} diff --git a/frontend/src/assets/pagination.module.scss b/frontend/src/assets/pagination.module.scss new file mode 100644 index 000000000..2b66d7510 --- /dev/null +++ b/frontend/src/assets/pagination.module.scss @@ -0,0 +1,3 @@ +.control { + --pagination-active-bg: var(--mantine-color-brand-filled); +} diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx index a29200e47..4e39dd9dc 100644 --- a/frontend/src/components/ErrorBoundary.tsx +++ b/frontend/src/components/ErrorBoundary.tsx @@ -1,5 +1,5 @@ -import UIError from "@/pages/errors/UIError"; import { Component, PropsWithChildren } from "react"; +import UIError from "@/pages/errors/UIError"; interface State { error: Error | null; diff --git a/frontend/src/components/Search.tsx b/frontend/src/components/Search.tsx index bc4a9f8d3..b506afee3 100644 --- a/frontend/src/components/Search.tsx +++ b/frontend/src/components/Search.tsx @@ -1,15 +1,10 @@ -import { useServerSearch } from "@/apis/hooks"; -import { useDebouncedValue } from "@/utilities"; +import { FunctionComponent, useMemo, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Autocomplete, ComboboxItem, OptionsFilter, Text } from "@mantine/core"; import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - Anchor, - Autocomplete, - createStyles, - SelectItemProps, -} from "@mantine/core"; -import { forwardRef, FunctionComponent, useMemo, useState } from "react"; -import { Link } from "react-router-dom"; +import { useServerSearch } from "@/apis/hooks"; +import { useDebouncedValue } from "@/utilities"; type SearchResultItem = { value: string; @@ -18,7 +13,7 @@ type SearchResultItem = { function useSearch(query: string) { const debouncedQuery = useDebouncedValue(query, 500); - const { data } = useServerSearch(debouncedQuery, debouncedQuery.length > 0); + const { data } = useServerSearch(debouncedQuery, debouncedQuery.length >= 0); return useMemo( () => @@ -31,7 +26,6 @@ function useSearch(query: string) { } else { throw new Error("Unknown search result"); } - return { value: `${v.title} (${v.year})`, link, @@ -41,59 +35,43 @@ function useSearch(query: string) { ); } -const useStyles = createStyles((theme) => { - return { - result: { - color: - theme.colorScheme === "light" - ? theme.colors.dark[8] - : theme.colors.gray[1], - }, - }; -}); - -type ResultCompProps = SelectItemProps & SearchResultItem; - -const ResultComponent = forwardRef( - ({ link, value }, ref) => { - const styles = useStyles(); +const optionsFilter: OptionsFilter = ({ options, search }) => { + const lowercaseSearch = search.toLowerCase(); + const trimmedSearch = search.trim(); + return (options as ComboboxItem[]).filter((option) => { return ( - - {value} - + option.value.toLowerCase().includes(lowercaseSearch) || + option.value + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase() + .includes(trimmedSearch) ); - }, -); + }); +}; const Search: FunctionComponent = () => { + const navigate = useNavigate(); const [query, setQuery] = useState(""); const results = useSearch(query); return ( } - itemComponent={ResultComponent} + leftSection={} + renderOption={(input) => {input.option.value}} placeholder="Search" size="sm" data={results} value={query} + scrollAreaProps={{ type: "auto" }} + maxDropdownHeight={400} onChange={setQuery} onBlur={() => setQuery("")} - filter={(value, item) => - item.value.toLowerCase().includes(value.toLowerCase().trim()) || - item.value - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .toLowerCase() - .includes(value.trim()) + filter={optionsFilter} + onOptionSubmit={(option) => + navigate(results.find((a) => a.value === option)?.link || "/") } > ); diff --git a/frontend/src/components/StateIcon.tsx b/frontend/src/components/StateIcon.tsx index f9683f63a..31e0b5243 100644 --- a/frontend/src/components/StateIcon.tsx +++ b/frontend/src/components/StateIcon.tsx @@ -1,4 +1,6 @@ -import { BuildKey } from "@/utilities"; +import { FunctionComponent } from "react"; +import { Group, List, Popover, Stack, Text } from "@mantine/core"; +import { useHover } from "@mantine/hooks"; import { faCheck, faCheckCircle, @@ -7,9 +9,7 @@ import { faTimes, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Group, List, Popover, Stack, Text } from "@mantine/core"; -import { useHover } from "@mantine/hooks"; -import { FunctionComponent } from "react"; +import { BuildKey } from "@/utilities"; interface StateIconProps { matches: string[]; @@ -31,7 +31,7 @@ const StateIcon: FunctionComponent = ({ return ; } else { return ( - + @@ -48,9 +48,9 @@ const StateIcon: FunctionComponent = ({ - - - + + + @@ -59,8 +59,8 @@ const StateIcon: FunctionComponent = ({ ))} - - + + diff --git a/frontend/src/components/SubtitleToolsMenu.tsx b/frontend/src/components/SubtitleToolsMenu.tsx index e36a1e9e1..545c87478 100644 --- a/frontend/src/components/SubtitleToolsMenu.tsx +++ b/frontend/src/components/SubtitleToolsMenu.tsx @@ -1,11 +1,5 @@ -import { useSubtitleAction } from "@/apis/hooks"; -import { ColorToolModal } from "@/components/forms/ColorToolForm"; -import { FrameRateModal } from "@/components/forms/FrameRateForm"; -import { TimeOffsetModal } from "@/components/forms/TimeOffsetForm"; -import { TranslationModal } from "@/components/forms/TranslationForm"; -import { useModals } from "@/modules/modals"; -import { ModalComponent } from "@/modules/modals/WithModal"; -import { task } from "@/modules/task"; +import { FunctionComponent, ReactElement, useCallback, useMemo } from "react"; +import { Divider, List, Menu, MenuProps, ScrollArea } from "@mantine/core"; import { faClock, faCode, @@ -23,8 +17,14 @@ import { IconDefinition, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Divider, List, Menu, MenuProps, ScrollArea } from "@mantine/core"; -import { FunctionComponent, ReactElement, useCallback, useMemo } from "react"; +import { useSubtitleAction } from "@/apis/hooks"; +import { ColorToolModal } from "@/components/forms/ColorToolForm"; +import { FrameRateModal } from "@/components/forms/FrameRateForm"; +import { TimeOffsetModal } from "@/components/forms/TimeOffsetForm"; +import { TranslationModal } from "@/components/forms/TranslationForm"; +import { useModals } from "@/modules/modals"; +import { ModalComponent } from "@/modules/modals/WithModal"; +import { task } from "@/modules/task"; import { SyncSubtitleModal } from "./forms/SyncSubtitleForm"; export interface ToolOptions { @@ -127,6 +127,8 @@ const SubtitleToolsMenu: FunctionComponent = ({ type: s.type, language: s.language, path: s.path, + hi: s.hi, + forced: s.forced, }; task.create(s.path, name, mutateAsync, { action, form }); }); @@ -148,7 +150,7 @@ const SubtitleToolsMenu: FunctionComponent = ({ } + leftSection={} onClick={() => { if (tool.modal) { modals.openContextModal(tool.modal, { selections }); @@ -164,7 +166,7 @@ const SubtitleToolsMenu: FunctionComponent = ({ Actions } + leftSection={} onClick={() => { onAction?.("search"); }} @@ -174,7 +176,7 @@ const SubtitleToolsMenu: FunctionComponent = ({ } + leftSection={} onClick={() => { modals.openConfirmModal({ title: "The following subtitles will be deleted", diff --git a/frontend/src/components/TextPopover.tsx b/frontend/src/components/TextPopover.tsx index 8fda5913e..03dd58700 100644 --- a/frontend/src/components/TextPopover.tsx +++ b/frontend/src/components/TextPopover.tsx @@ -1,7 +1,7 @@ +import { FunctionComponent, ReactElement } from "react"; import { Tooltip, TooltipProps } from "@mantine/core"; import { useHover } from "@mantine/hooks"; import { isNull, isUndefined } from "lodash"; -import { FunctionComponent, ReactElement } from "react"; interface TextPopoverProps { children: ReactElement; @@ -21,7 +21,12 @@ const TextPopover: FunctionComponent = ({ } return ( - + {children} ); diff --git a/frontend/src/components/async/Lazy.tsx b/frontend/src/components/async/Lazy.tsx index 2a0496223..317c0feb3 100644 --- a/frontend/src/components/async/Lazy.tsx +++ b/frontend/src/components/async/Lazy.tsx @@ -1,5 +1,5 @@ -import { LoadingOverlay } from "@mantine/core"; import { FunctionComponent, PropsWithChildren, Suspense } from "react"; +import { LoadingOverlay } from "@mantine/core"; const Lazy: FunctionComponent = ({ children }) => { return }>{children}; diff --git a/frontend/src/components/async/MutateAction.tsx b/frontend/src/components/async/MutateAction.tsx index 920fe4ff3..92c102ea9 100644 --- a/frontend/src/components/async/MutateAction.tsx +++ b/frontend/src/components/async/MutateAction.tsx @@ -1,7 +1,7 @@ import { useCallback, useState } from "react"; -import { UseMutationResult } from "react-query"; -import { Action } from "../inputs"; -import { ActionProps } from "../inputs/Action"; +import { UseMutationResult } from "@tanstack/react-query"; +import { Action } from "@/components/inputs"; +import { ActionProps } from "@/components/inputs/Action"; type MutateActionProps = Omit< ActionProps, @@ -16,7 +16,6 @@ type MutateActionProps = Omit< function MutateAction({ mutation, - noReset, onSuccess, onError, args, diff --git a/frontend/src/components/async/MutateButton.tsx b/frontend/src/components/async/MutateButton.tsx index 8d0f68541..908c2dfda 100644 --- a/frontend/src/components/async/MutateButton.tsx +++ b/frontend/src/components/async/MutateButton.tsx @@ -1,6 +1,6 @@ -import { Button, ButtonProps } from "@mantine/core"; import { useCallback, useState } from "react"; -import { UseMutationResult } from "react-query"; +import { Button, ButtonProps } from "@mantine/core"; +import { UseMutationResult } from "@tanstack/react-query"; type MutateButtonProps = Omit< ButtonProps, @@ -15,7 +15,6 @@ type MutateButtonProps = Omit< function MutateButton({ mutation, - noReset, onSuccess, onError, args, diff --git a/frontend/src/components/async/QueryOverlay.tsx b/frontend/src/components/async/QueryOverlay.tsx index 24b95ab18..1672989ff 100644 --- a/frontend/src/components/async/QueryOverlay.tsx +++ b/frontend/src/components/async/QueryOverlay.tsx @@ -1,7 +1,7 @@ -import { LoadingProvider } from "@/contexts"; -import { LoadingOverlay } from "@mantine/core"; import { FunctionComponent, ReactNode } from "react"; -import { UseQueryResult } from "react-query"; +import { LoadingOverlay } from "@mantine/core"; +import { UseQueryResult } from "@tanstack/react-query"; +import { LoadingProvider } from "@/contexts"; interface QueryOverlayProps { result: UseQueryResult; @@ -12,7 +12,7 @@ interface QueryOverlayProps { const QueryOverlay: FunctionComponent = ({ children, global = false, - result: { isLoading, isError, error }, + result: { isLoading }, }) => { return ( diff --git a/frontend/src/components/bazarr/AudioList.tsx b/frontend/src/components/bazarr/AudioList.tsx index ac9cce743..f0dc07c0d 100644 --- a/frontend/src/components/bazarr/AudioList.tsx +++ b/frontend/src/components/bazarr/AudioList.tsx @@ -1,6 +1,7 @@ -import { BuildKey } from "@/utilities"; -import { Badge, BadgeProps, Group, GroupProps } from "@mantine/core"; import { FunctionComponent } from "react"; +import { Badge, BadgeProps, Group, GroupProps } from "@mantine/core"; +import { BuildKey } from "@/utilities"; +import { normalizeAudioLanguage } from "@/utilities/languages"; export type AudioListProps = GroupProps & { audios: Language.Info[]; @@ -13,10 +14,10 @@ const AudioList: FunctionComponent = ({ ...group }) => { return ( - + {audios.map((audio, idx) => ( - {audio.name} + {normalizeAudioLanguage(audio.name)} ))} diff --git a/frontend/src/components/bazarr/HistoryIcon.tsx b/frontend/src/components/bazarr/HistoryIcon.tsx index e6c0f2411..add0cd1fd 100644 --- a/frontend/src/components/bazarr/HistoryIcon.tsx +++ b/frontend/src/components/bazarr/HistoryIcon.tsx @@ -1,3 +1,5 @@ +import { FunctionComponent } from "react"; +import { Tooltip } from "@mantine/core"; import { faClock, faClosedCaptioning, @@ -9,8 +11,6 @@ import { faUser, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Tooltip } from "@mantine/core"; -import { FunctionComponent } from "react"; enum HistoryAction { Delete = 0, diff --git a/frontend/src/components/bazarr/Language.test.tsx b/frontend/src/components/bazarr/Language.test.tsx index 9e0e0fab8..2cad5d4c8 100644 --- a/frontend/src/components/bazarr/Language.test.tsx +++ b/frontend/src/components/bazarr/Language.test.tsx @@ -1,5 +1,5 @@ -import { rawRender, screen } from "@/tests"; import { describe, it } from "vitest"; +import { render, screen } from "@/tests"; import { Language } from "."; describe("Language text", () => { @@ -9,13 +9,13 @@ describe("Language text", () => { }; it("should show short text", () => { - rawRender(); + render(); expect(screen.getByText(testLanguage.code2)).toBeDefined(); }); it("should show long text", () => { - rawRender(); + render(); expect(screen.getByText(testLanguage.name)).toBeDefined(); }); @@ -23,7 +23,7 @@ describe("Language text", () => { const testLanguageWithHi: Language.Info = { ...testLanguage, hi: true }; it("should show short text with HI", () => { - rawRender(); + render(); const expectedText = `${testLanguageWithHi.code2}:HI`; @@ -31,7 +31,7 @@ describe("Language text", () => { }); it("should show long text with HI", () => { - rawRender(); + render(); const expectedText = `${testLanguageWithHi.name} HI`; @@ -44,7 +44,7 @@ describe("Language text", () => { }; it("should show short text with Forced", () => { - rawRender(); + render(); const expectedText = `${testLanguageWithHi.code2}:Forced`; @@ -52,9 +52,7 @@ describe("Language text", () => { }); it("should show long text with Forced", () => { - rawRender( - , - ); + render(); const expectedText = `${testLanguageWithHi.name} Forced`; @@ -75,7 +73,7 @@ describe("Language list", () => { ]; it("should show all languages", () => { - rawRender(); + render(); elements.forEach((value) => { expect(screen.getByText(value.name)).toBeDefined(); diff --git a/frontend/src/components/bazarr/Language.tsx b/frontend/src/components/bazarr/Language.tsx index e5627c82e..6315d9102 100644 --- a/frontend/src/components/bazarr/Language.tsx +++ b/frontend/src/components/bazarr/Language.tsx @@ -1,6 +1,6 @@ -import { BuildKey } from "@/utilities"; -import { Badge, Group, Text, TextProps } from "@mantine/core"; import { FunctionComponent, useMemo } from "react"; +import { Badge, Group, Text, TextProps } from "@mantine/core"; +import { BuildKey } from "@/utilities"; type LanguageTextProps = TextProps & { value: Language.Info; @@ -49,7 +49,7 @@ type LanguageListProps = { const LanguageList: FunctionComponent = ({ value }) => { return ( - + {value.map((v) => ( {v.name} ))} diff --git a/frontend/src/components/bazarr/LanguageProfile.tsx b/frontend/src/components/bazarr/LanguageProfile.tsx index 75b7b73ca..a234268c3 100644 --- a/frontend/src/components/bazarr/LanguageProfile.tsx +++ b/frontend/src/components/bazarr/LanguageProfile.tsx @@ -1,5 +1,5 @@ -import { useLanguageProfiles } from "@/apis/hooks"; import { FunctionComponent, useMemo } from "react"; +import { useLanguageProfiles } from "@/apis/hooks"; interface Props { index: number | null; diff --git a/frontend/src/components/bazarr/LanguageSelector.tsx b/frontend/src/components/bazarr/LanguageSelector.tsx index c2219ca7c..8954403bd 100644 --- a/frontend/src/components/bazarr/LanguageSelector.tsx +++ b/frontend/src/components/bazarr/LanguageSelector.tsx @@ -1,7 +1,7 @@ +import { FunctionComponent, useMemo } from "react"; import { useLanguages } from "@/apis/hooks"; import { Selector, SelectorProps } from "@/components/inputs"; import { useSelectorOptions } from "@/utilities"; -import { FunctionComponent, useMemo } from "react"; interface LanguageSelectorProps extends Omit, "options" | "getkey"> { diff --git a/frontend/src/components/forms/ColorToolForm.tsx b/frontend/src/components/forms/ColorToolForm.tsx index a37819bee..9deac9bf4 100644 --- a/frontend/src/components/forms/ColorToolForm.tsx +++ b/frontend/src/components/forms/ColorToolForm.tsx @@ -1,11 +1,11 @@ +import { FunctionComponent } from "react"; +import { Button, Divider, Stack } from "@mantine/core"; +import { useForm } from "@mantine/form"; import { useSubtitleAction } from "@/apis/hooks"; import { Selector, SelectorOption } from "@/components"; import { useModals, withModal } from "@/modules/modals"; import { task } from "@/modules/task"; import FormUtils from "@/utilities/form"; -import { Button, Divider, Stack } from "@mantine/core"; -import { useForm } from "@mantine/form"; -import { FunctionComponent } from "react"; const TaskName = "Changing Color"; diff --git a/frontend/src/components/forms/FrameRateForm.tsx b/frontend/src/components/forms/FrameRateForm.tsx index 7e7eca24c..7c57daf28 100644 --- a/frontend/src/components/forms/FrameRateForm.tsx +++ b/frontend/src/components/forms/FrameRateForm.tsx @@ -1,10 +1,10 @@ +import { FunctionComponent } from "react"; +import { Button, Divider, Group, NumberInput, Stack } from "@mantine/core"; +import { useForm } from "@mantine/form"; import { useSubtitleAction } from "@/apis/hooks"; import { useModals, withModal } from "@/modules/modals"; import { task } from "@/modules/task"; import FormUtils from "@/utilities/form"; -import { Button, Divider, Group, NumberInput, Stack } from "@mantine/core"; -import { useForm } from "@mantine/form"; -import { FunctionComponent } from "react"; const TaskName = "Changing Frame Rate"; @@ -55,15 +55,17 @@ const FrameRateForm: FunctionComponent = ({ selections, onSubmit }) => { })} > - + diff --git a/frontend/src/components/forms/ItemEditForm.tsx b/frontend/src/components/forms/ItemEditForm.tsx index 9f3856d54..392338500 100644 --- a/frontend/src/components/forms/ItemEditForm.tsx +++ b/frontend/src/components/forms/ItemEditForm.tsx @@ -1,11 +1,11 @@ +import { FunctionComponent, useMemo } from "react"; +import { Button, Divider, Group, LoadingOverlay, Stack } from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { UseMutationResult } from "@tanstack/react-query"; import { useLanguageProfiles } from "@/apis/hooks"; import { MultiSelector, Selector } from "@/components/inputs"; import { useModals, withModal } from "@/modules/modals"; import { GetItemId, useSelectorOptions } from "@/utilities"; -import { Button, Divider, Group, LoadingOverlay, Stack } from "@mantine/core"; -import { useForm } from "@mantine/form"; -import { FunctionComponent, useMemo } from "react"; -import { UseMutationResult } from "react-query"; interface Props { mutation: UseMutationResult; @@ -21,7 +21,7 @@ const ItemEditForm: FunctionComponent = ({ onCancel, }) => { const { data, isFetching } = useLanguageProfiles(); - const { isLoading, mutate } = mutation; + const { isPending, mutate } = mutation; const modals = useModals(); const profileOptions = useSelectorOptions( @@ -47,7 +47,7 @@ const ItemEditForm: FunctionComponent = ({ (v) => v.code2, ); - const isOverlayVisible = isLoading || isFetching || item === null; + const isOverlayVisible = isPending || isFetching || item === null; return ( = ({ label="Languages Profile" > - + { diff --git a/frontend/src/components/forms/MovieUploadForm.tsx b/frontend/src/components/forms/MovieUploadForm.tsx index b51614770..8e318d7ad 100644 --- a/frontend/src/components/forms/MovieUploadForm.tsx +++ b/frontend/src/components/forms/MovieUploadForm.tsx @@ -1,37 +1,35 @@ -import { useMovieSubtitleModification } from "@/apis/hooks"; -import { useModals, withModal } from "@/modules/modals"; -import { TaskGroup, task } from "@/modules/task"; -import { useTableStyles } from "@/styles"; -import { useArrayAction, useSelectorOptions } from "@/utilities"; -import FormUtils from "@/utilities/form"; -import { - useLanguageProfileBy, - useProfileItemsToLanguages, -} from "@/utilities/languages"; -import { - faCheck, - faCircleNotch, - faInfoCircle, - faTimes, - faTrash, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { FunctionComponent, useEffect, useMemo } from "react"; import { Button, Checkbox, - createStyles, Divider, MantineColor, Stack, Text, } from "@mantine/core"; import { useForm } from "@mantine/form"; +import { + faCheck, + faCircleNotch, + faInfoCircle, + faTimes, + faTrash, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ColumnDef } from "@tanstack/react-table"; import { isString } from "lodash"; -import { FunctionComponent, useEffect, useMemo } from "react"; -import { Column } from "react-table"; -import TextPopover from "../TextPopover"; -import { Action, Selector } from "../inputs"; -import { SimpleTable } from "../tables"; +import { useMovieSubtitleModification } from "@/apis/hooks"; +import { Action, Selector } from "@/components/inputs"; +import SimpleTable from "@/components/tables/SimpleTable"; +import TextPopover from "@/components/TextPopover"; +import { useModals, withModal } from "@/modules/modals"; +import { task, TaskGroup } from "@/modules/task"; +import { useArrayAction, useSelectorOptions } from "@/utilities"; +import FormUtils from "@/utilities/form"; +import { + useLanguageProfileBy, + useProfileItemsToLanguages, +} from "@/utilities/languages"; type SubtitleFile = { file: File; @@ -79,21 +77,12 @@ interface Props { onComplete?: () => void; } -const useStyles = createStyles((theme) => { - return { - wrapper: { - overflowWrap: "anywhere", - }, - }; -}); - const MovieUploadForm: FunctionComponent = ({ files, movie, onComplete, }) => { const modals = useModals(); - const { classes } = useStyles(); const profile = useLanguageProfileBy(movie.profileId); @@ -154,63 +143,77 @@ const MovieUploadForm: FunctionComponent = ({ }); }); - const columns = useMemo[]>( - () => [ - { - accessor: "validateResult", - Cell: ({ cell: { value } }) => { - const icon = useMemo(() => { - switch (value?.state) { - case "valid": - return faCheck; - case "warning": - return faInfoCircle; - case "error": - return faTimes; - default: - return faCircleNotch; - } - }, [value?.state]); + const ValidateResultCell = ({ + validateResult, + }: { + validateResult: SubtitleValidateResult | undefined; + }) => { + const icon = useMemo(() => { + switch (validateResult?.state) { + case "valid": + return faCheck; + case "warning": + return faInfoCircle; + case "error": + return faTimes; + default: + return faCircleNotch; + } + }, [validateResult?.state]); - const color = useMemo(() => { - switch (value?.state) { - case "valid": - return "green"; - case "warning": - return "yellow"; - case "error": - return "red"; - default: - return undefined; - } - }, [value?.state]); + const color = useMemo(() => { + switch (validateResult?.state) { + case "valid": + return "green"; + case "warning": + return "yellow"; + case "error": + return "red"; + default: + return undefined; + } + }, [validateResult?.state]); - return ( - - - - - - ); + return ( + + + + + + ); + }; + + const columns = useMemo[]>( + () => [ + { + id: "validateResult", + cell: ({ + row: { + original: { validateResult }, + }, + }) => { + return ; }, }, { - Header: "File", + header: "File", id: "filename", - accessor: "file", - Cell: ({ value }) => { - const { classes } = useTableStyles(); - - return {value.name}; + accessorKey: "file", + cell: ({ + row: { + original: { file }, + }, + }) => { + return {file.name}; }, }, { - Header: "Forced", - accessor: "forced", - Cell: ({ row: { original, index }, value }) => { + header: "Forced", + accessorKey: "forced", + cell: ({ row: { original, index } }) => { return ( { action.mutate(index, { ...original, forced: checked }); }} @@ -219,12 +222,12 @@ const MovieUploadForm: FunctionComponent = ({ }, }, { - Header: "HI", - accessor: "hi", - Cell: ({ row: { original, index }, value }) => { + header: "HI", + accessorKey: "hi", + cell: ({ row: { original, index } }) => { return ( { action.mutate(index, { ...original, hi: checked }); }} @@ -233,15 +236,14 @@ const MovieUploadForm: FunctionComponent = ({ }, }, { - Header: "Language", - accessor: "language", - Cell: ({ row: { original, index }, value }) => { - const { classes } = useTableStyles(); + header: "Language", + accessorKey: "language", + cell: ({ row: { original, index } }) => { return ( { action.mutate(index, { ...original, language: item }); }} @@ -251,13 +253,12 @@ const MovieUploadForm: FunctionComponent = ({ }, { id: "action", - accessor: "file", - Cell: ({ row: { index } }) => { + cell: ({ row: { index } }) => { return ( action.remove(index)} > ); @@ -289,7 +290,7 @@ const MovieUploadForm: FunctionComponent = ({ modals.closeSelf(); })} > - + Upload diff --git a/frontend/src/components/forms/ProfileEditForm.module.scss b/frontend/src/components/forms/ProfileEditForm.module.scss new file mode 100644 index 000000000..3d4a8e177 --- /dev/null +++ b/frontend/src/components/forms/ProfileEditForm.module.scss @@ -0,0 +1,13 @@ +.content { + @include smaller-than($mantine-breakpoint-md) { + padding: 0; + } +} + +.evenly { + flex-wrap: wrap; + + & > div { + flex: 1; + } +} diff --git a/frontend/src/components/forms/ProfileEditForm.tsx b/frontend/src/components/forms/ProfileEditForm.tsx index eecacd73e..267951fcb 100644 --- a/frontend/src/components/forms/ProfileEditForm.tsx +++ b/frontend/src/components/forms/ProfileEditForm.tsx @@ -1,14 +1,9 @@ -import { Action, Selector, SelectorOption, SimpleTable } from "@/components"; -import { useModals, withModal } from "@/modules/modals"; -import { useTableStyles } from "@/styles"; -import { useArrayAction, useSelectorOptions } from "@/utilities"; -import { LOG } from "@/utilities/console"; -import FormUtils from "@/utilities/form"; -import { faTrash } from "@fortawesome/free-solid-svg-icons"; +import React, { FunctionComponent, useCallback, useMemo } from "react"; import { Accordion, Button, Checkbox, + Flex, Select, Stack, Switch, @@ -16,9 +11,16 @@ import { TextInput, } from "@mantine/core"; import { useForm } from "@mantine/form"; -import { FunctionComponent, useCallback, useMemo } from "react"; -import { Column } from "react-table"; -import ChipInput from "../inputs/ChipInput"; +import { faTrash } from "@fortawesome/free-solid-svg-icons"; +import { ColumnDef } from "@tanstack/react-table"; +import { Action, Selector, SelectorOption } from "@/components"; +import ChipInput from "@/components/inputs/ChipInput"; +import SimpleTable from "@/components/tables/SimpleTable"; +import { useModals, withModal } from "@/modules/modals"; +import { useArrayAction, useSelectorOptions } from "@/utilities"; +import { LOG } from "@/utilities/console"; +import FormUtils from "@/utilities/form"; +import styles from "./ProfileEditForm.module.scss"; export const anyCutoff = 65535; @@ -71,9 +73,16 @@ const ProfileEditForm: FunctionComponent = ({ (value) => value.length > 0, "Must have a name", ), + tag: FormUtils.validation((value) => { + if (!value) { + return true; + } + + return /^[a-z_0-9-]+$/.test(value); + }, "Only lowercase alphanumeric characters, underscores (_) and hyphens (-) are allowed"), items: FormUtils.validation( (value) => value.length > 0, - "Must contain at lease 1 language", + "Must contain at least 1 language", ), }, }); @@ -145,78 +154,88 @@ const ProfileEditForm: FunctionComponent = ({ } }, [form, languages]); - const columns = useMemo[]>( + const LanguageCell = React.memo( + ({ item, index }: { item: Language.ProfileItem; index: number }) => { + const code = useMemo( + () => + languageOptions.options.find((l) => l.value.code2 === item.language) + ?.value ?? null, + [item.language], + ); + + return ( + { + if (value) { + item.language = value.code2; + action.mutate(index, { ...item, language: value.code2 }); + } + }} + > + ); + }, + ); + + const SubtitleTypeCell = React.memo( + ({ item, index }: { item: Language.ProfileItem; index: number }) => { + const selectValue = useMemo(() => { + if (item.forced === "True") { + return "forced"; + } else if (item.hi === "True") { + return "hi"; + } else { + return "normal"; + } + }, [item.forced, item.hi]); + + return ( + { + if (value) { + action.mutate(index, { + ...item, + hi: value === "hi" ? "True" : "False", + forced: value === "forced" ? "True" : "False", + }); + } + }} + > + ); + }, + ); + + const columns = useMemo[]>( () => [ { - Header: "ID", - accessor: "id", + header: "ID", + accessorKey: "id", }, { - Header: "Language", - accessor: "language", - Cell: ({ value: code, row: { original: item, index } }) => { - const language = useMemo( - () => - languageOptions.options.find((l) => l.value.code2 === code) - ?.value ?? null, - [code], - ); - - const { classes } = useTableStyles(); - - return ( - { - if (value) { - item.language = value.code2; - action.mutate(index, { ...item, language: value.code2 }); - } - }} - > - ); + header: "Language", + accessorKey: "language", + cell: ({ row: { original: item, index } }) => { + return ; }, }, { - Header: "Subtitles Type", - accessor: "forced", - Cell: ({ row: { original: item, index }, value }) => { - const selectValue = useMemo(() => { - if (item.forced === "True") { - return "forced"; - } else if (item.hi === "True") { - return "hi"; - } else { - return "normal"; - } - }, [item.forced, item.hi]); - - return ( - { - if (value) { - action.mutate(index, { - ...item, - hi: value === "hi" ? "True" : "False", - forced: value === "forced" ? "True" : "False", - }); - } - }} - > - ); + header: "Subtitles Type", + accessorKey: "forced", + cell: ({ row: { original: item, index } }) => { + return ; }, }, { - Header: "Exclude If Matching Audio", - accessor: "audio_exclude", - Cell: ({ row: { original: item, index }, value }) => { + header: "Exclude If Matching Audio", + accessorKey: "audio_exclude", + cell: ({ row: { original: item, index } }) => { return ( { action.mutate(index, { ...item, @@ -230,20 +249,19 @@ const ProfileEditForm: FunctionComponent = ({ }, { id: "action", - accessor: "id", - Cell: ({ row }) => { + cell: ({ row }) => { return ( action.remove(row.index)} > ); }, }, ], - [action, languageOptions], + [action, LanguageCell, SubtitleTypeCell], ); return ( @@ -255,29 +273,40 @@ const ProfileEditForm: FunctionComponent = ({ })} > - + + + + form.setFieldValue( + "tag", + (prev) => + prev?.toLowerCase().trim().replace(/\s+/g, "_") ?? undefined, + ) + } + > + ({ - content: { - [theme.fn.smallerThan("md")]: { - padding: 0, - }, - }, - })} + className={styles.content} > - {form.errors.items} - + Add Language + {form.errors.items} { - return { - wrapper: { - overflowWrap: "anywhere", - }, - }; -}); - const SeriesUploadForm: FunctionComponent = ({ series, files, onComplete, }) => { const modals = useModals(); - const { classes } = useStyles(); const episodes = useEpisodesBySeriesId(series.sonarrSeriesId); const episodeOptions = useSelectorOptions( episodes.data ?? [], @@ -180,62 +169,79 @@ const SeriesUploadForm: FunctionComponent = ({ } }, [action, episodes.data, infos.data]); - const columns = useMemo[]>( - () => [ - { - accessor: "validateResult", - Cell: ({ cell: { value } }) => { - const icon = useMemo(() => { - switch (value?.state) { - case "valid": - return faCheck; - case "warning": - return faInfoCircle; - case "error": - return faTimes; - default: - return faCircleNotch; - } - }, [value?.state]); + const ValidateResultCell = ({ + validateResult, + }: { + validateResult: SubtitleValidateResult | undefined; + }) => { + const icon = useMemo(() => { + switch (validateResult?.state) { + case "valid": + return faCheck; + case "warning": + return faInfoCircle; + case "error": + return faTimes; + default: + return faCircleNotch; + } + }, [validateResult?.state]); - const color = useMemo(() => { - switch (value?.state) { - case "valid": - return "green"; - case "warning": - return "yellow"; - case "error": - return "red"; - default: - return undefined; - } - }, [value?.state]); + const color = useMemo(() => { + switch (validateResult?.state) { + case "valid": + return "green"; + case "warning": + return "yellow"; + case "error": + return "red"; + default: + return undefined; + } + }, [validateResult?.state]); - return ( - - - - - - ); + return ( + + + + + + ); + }; + + const columns = useMemo[]>( + () => [ + { + id: "validateResult", + cell: ({ + row: { + original: { validateResult }, + }, + }) => { + return ; }, }, { - Header: "File", + header: "File", id: "filename", - accessor: "file", - Cell: ({ value: { name } }) => { - const { classes } = useTableStyles(); - return {name}; + accessorKey: "file", + cell: ({ + row: { + original: { + file: { name }, + }, + }, + }) => { + return {name}; }, }, { - Header: "Forced", - accessor: "forced", - Cell: ({ row: { original, index }, value }) => { + header: "Forced", + accessorKey: "forced", + cell: ({ row: { original, index } }) => { return ( { action.mutate(index, { ...original, @@ -248,12 +254,12 @@ const SeriesUploadForm: FunctionComponent = ({ }, }, { - Header: "HI", - accessor: "hi", - Cell: ({ row: { original, index }, value }) => { + header: "HI", + accessorKey: "hi", + cell: ({ row: { original, index } }) => { return ( { action.mutate(index, { ...original, @@ -266,7 +272,7 @@ const SeriesUploadForm: FunctionComponent = ({ }, }, { - Header: ( + header: () => ( = ({ }} > ), - accessor: "language", - Cell: ({ row: { original, index }, value }) => { - const { classes } = useTableStyles(); + accessorKey: "language", + cell: ({ row: { original, index } }) => { return ( { action.mutate(index, { ...original, language: item }); }} @@ -298,18 +303,17 @@ const SeriesUploadForm: FunctionComponent = ({ }, { id: "episode", - Header: "Episode", - accessor: "episode", - Cell: ({ value, row }) => { - const { classes } = useTableStyles(); + header: "Episode", + accessorKey: "episode", + cell: ({ row: { original, index } }) => { return ( { - action.mutate(row.index, { ...row.original, episode: item }); + action.mutate(index, { ...original, episode: item }); }} > ); @@ -317,13 +321,12 @@ const SeriesUploadForm: FunctionComponent = ({ }, { id: "action", - accessor: "file", - Cell: ({ row: { index } }) => { + cell: ({ row: { index } }) => { return ( action.remove(index)} > ); @@ -368,7 +371,7 @@ const SeriesUploadForm: FunctionComponent = ({ modals.closeSelf(); })} > - + Upload diff --git a/frontend/src/components/forms/SyncSubtitleForm.tsx b/frontend/src/components/forms/SyncSubtitleForm.tsx index b5136fc85..63953fb2d 100644 --- a/frontend/src/components/forms/SyncSubtitleForm.tsx +++ b/frontend/src/components/forms/SyncSubtitleForm.tsx @@ -1,20 +1,23 @@ /* eslint-disable camelcase */ - +import { FunctionComponent } from "react"; +import { Alert, Button, Checkbox, Divider, Stack, Text } from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useRefTracksByEpisodeId, useRefTracksByMovieId, useSubtitleAction, } from "@/apis/hooks"; +import { + GroupedSelector, + GroupedSelectorOptions, + Selector, +} from "@/components/inputs"; import { useModals, withModal } from "@/modules/modals"; import { task } from "@/modules/task"; import { syncMaxOffsetSecondsOptions } from "@/pages/Settings/Subtitles/options"; -import { toPython } from "@/utilities"; -import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Alert, Button, Checkbox, Divider, Stack, Text } from "@mantine/core"; -import { useForm } from "@mantine/form"; -import { FunctionComponent } from "react"; -import { Selector, SelectorOption } from "../inputs"; +import { fromPython, toPython } from "@/utilities"; const TaskName = "Syncing Subtitle"; @@ -37,15 +40,21 @@ function useReferencedSubtitles( const mediaData = mediaType === "episode" ? episodeData : movieData; - const subtitles: { group: string; value: string; label: string }[] = []; + const subtitles: GroupedSelectorOptions[] = []; if (!mediaData.data) { return []; } else { if (mediaData.data.audio_tracks.length > 0) { + const embeddedAudioGroup: GroupedSelectorOptions = { + group: "Embedded audio tracks", + items: [], + }; + + subtitles.push(embeddedAudioGroup); + mediaData.data.audio_tracks.forEach((item) => { - subtitles.push({ - group: "Embedded audio tracks", + embeddedAudioGroup.items.push({ value: item.stream, label: `${item.name || item.language} (${item.stream})`, }); @@ -53,9 +62,15 @@ function useReferencedSubtitles( } if (mediaData.data.embedded_subtitles_tracks.length > 0) { + const embeddedSubtitlesTrackGroup: GroupedSelectorOptions = { + group: "Embedded subtitles tracks", + items: [], + }; + + subtitles.push(embeddedSubtitlesTrackGroup); + mediaData.data.embedded_subtitles_tracks.forEach((item) => { - subtitles.push({ - group: "Embedded subtitles tracks", + embeddedSubtitlesTrackGroup.items.push({ value: item.stream, label: `${item.name || item.language} (${item.stream})`, }); @@ -63,10 +78,16 @@ function useReferencedSubtitles( } if (mediaData.data.external_subtitles_tracks.length > 0) { + const externalSubtitlesFilesGroup: GroupedSelectorOptions = { + group: "External Subtitles files", + items: [], + }; + + subtitles.push(externalSubtitlesFilesGroup); + mediaData.data.external_subtitles_tracks.forEach((item) => { if (item) { - subtitles.push({ - group: "External Subtitles files", + externalSubtitlesFilesGroup.items.push({ value: item.path, label: item.name, }); @@ -88,6 +109,8 @@ interface FormValues { maxOffsetSeconds?: string; noFixFramerate: boolean; gss: boolean; + hi?: boolean; + forced?: boolean; } const SyncSubtitleForm: FunctionComponent = ({ @@ -101,20 +124,20 @@ const SyncSubtitleForm: FunctionComponent = ({ const { mutateAsync } = useSubtitleAction(); const modals = useModals(); - const mediaType = selections[0].type; - const mediaId = selections[0].id; - const subtitlesPath = selections[0].path; + const subtitle = selections[0]; - const subtitles: SelectorOption[] = useReferencedSubtitles( - mediaType, - mediaId, - subtitlesPath, - ); + const mediaType = subtitle.type; + const mediaId = subtitle.id; + const subtitlesPath = subtitle.path; + + const subtitles = useReferencedSubtitles(mediaType, mediaId, subtitlesPath); const form = useForm({ initialValues: { noFixFramerate: false, gss: false, + hi: fromPython(subtitle.hi), + forced: fromPython(subtitle.forced), }, }); @@ -145,14 +168,14 @@ const SyncSubtitleForm: FunctionComponent = ({ > {selections.length} subtitles selected - + > = ({ selections, onSubmit }) => { })} > - + form.setValues((f) => ({ ...f, positive: !f.positive })) } diff --git a/frontend/src/components/forms/TranslationForm.tsx b/frontend/src/components/forms/TranslationForm.tsx index 976b2f72f..20aa08478 100644 --- a/frontend/src/components/forms/TranslationForm.tsx +++ b/frontend/src/components/forms/TranslationForm.tsx @@ -1,14 +1,14 @@ +import { FunctionComponent, useMemo } from "react"; +import { Alert, Button, Divider, Stack } from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { isObject } from "lodash"; import { useSubtitleAction } from "@/apis/hooks"; +import { Selector } from "@/components/inputs"; import { useModals, withModal } from "@/modules/modals"; import { task } from "@/modules/task"; import { useSelectorOptions } from "@/utilities"; import FormUtils from "@/utilities/form"; import { useEnabledLanguages } from "@/utilities/languages"; -import { Alert, Button, Divider, Stack } from "@mantine/core"; -import { useForm } from "@mantine/form"; -import { isObject } from "lodash"; -import { FunctionComponent, useMemo } from "react"; -import { Selector } from "../inputs"; const TaskName = "Translating Subtitles"; diff --git a/frontend/src/components/index.tsx b/frontend/src/components/index.tsx index c3d7b4763..5ea97cf04 100644 --- a/frontend/src/components/index.tsx +++ b/frontend/src/components/index.tsx @@ -1,4 +1,4 @@ export { default as Search } from "./Search"; export * from "./inputs"; export * from "./tables"; -export { default as Toolbox } from "./toolbox"; +export { default as Toolbox } from "./toolbox/Toolbox"; diff --git a/frontend/src/components/inputs/Action.test.tsx b/frontend/src/components/inputs/Action.test.tsx index 189aca076..dc8972630 100644 --- a/frontend/src/components/inputs/Action.test.tsx +++ b/frontend/src/components/inputs/Action.test.tsx @@ -1,7 +1,7 @@ -import { rawRender, screen } from "@/tests"; import { faStickyNote } from "@fortawesome/free-regular-svg-icons"; import userEvent from "@testing-library/user-event"; import { describe, it, vitest } from "vitest"; +import { render, screen } from "@/tests"; import Action from "./Action"; const testLabel = "Test Label"; @@ -9,7 +9,7 @@ const testIcon = faStickyNote; describe("Action button", () => { it("should be a button", () => { - rawRender(); + render(); const element = screen.getByRole("button", { name: testLabel }); expect(element.getAttribute("type")).toEqual("button"); @@ -17,7 +17,7 @@ describe("Action button", () => { }); it("should show icon", () => { - rawRender(); + render(); // TODO: use getBy... const element = screen.getByRole("img", { hidden: true }); @@ -27,7 +27,7 @@ describe("Action button", () => { it("should call on-click event when clicked", async () => { const onClickFn = vitest.fn(); - rawRender( + render( , ); diff --git a/frontend/src/components/inputs/Action.tsx b/frontend/src/components/inputs/Action.tsx index 236baf112..95477f090 100644 --- a/frontend/src/components/inputs/Action.tsx +++ b/frontend/src/components/inputs/Action.tsx @@ -1,15 +1,15 @@ -import { IconDefinition } from "@fortawesome/fontawesome-common-types"; -import { - FontAwesomeIcon, - FontAwesomeIconProps, -} from "@fortawesome/react-fontawesome"; +import { forwardRef } from "react"; import { ActionIcon, ActionIconProps, Tooltip, TooltipProps, } from "@mantine/core"; -import { forwardRef } from "react"; +import { IconDefinition } from "@fortawesome/fontawesome-common-types"; +import { + FontAwesomeIcon, + FontAwesomeIconProps, +} from "@fortawesome/react-fontawesome"; export type ActionProps = MantineComp & { icon: IconDefinition; diff --git a/frontend/src/components/inputs/ChipInput.test.tsx b/frontend/src/components/inputs/ChipInput.test.tsx index cb52ee30c..4035966fc 100644 --- a/frontend/src/components/inputs/ChipInput.test.tsx +++ b/frontend/src/components/inputs/ChipInput.test.tsx @@ -1,6 +1,6 @@ -import { rawRender, screen } from "@/tests"; import userEvent from "@testing-library/user-event"; import { describe, it, vitest } from "vitest"; +import { render, screen } from "@/tests"; import ChipInput from "./ChipInput"; describe("ChipInput", () => { @@ -8,7 +8,7 @@ describe("ChipInput", () => { // TODO: Support default value it.skip("should works with default value", () => { - rawRender(); + render(); existedValues.forEach((value) => { expect(screen.getByText(value)).toBeDefined(); @@ -16,7 +16,7 @@ describe("ChipInput", () => { }); it("should works with value", () => { - rawRender(); + render(); existedValues.forEach((value) => { expect(screen.getByText(value)).toBeDefined(); @@ -29,9 +29,7 @@ describe("ChipInput", () => { expect(values).toContain(typedValue); }); - rawRender( - , - ); + render(); const element = screen.getByRole("searchbox"); diff --git a/frontend/src/components/inputs/ChipInput.tsx b/frontend/src/components/inputs/ChipInput.tsx index 4308f7189..1fa57084c 100644 --- a/frontend/src/components/inputs/ChipInput.tsx +++ b/frontend/src/components/inputs/ChipInput.tsx @@ -1,35 +1,29 @@ -import { useSelectorOptions } from "@/utilities"; import { FunctionComponent } from "react"; -import { MultiSelector, MultiSelectorProps } from "./Selector"; +import { TagsInput } from "@mantine/core"; -export type ChipInputProps = Omit< - MultiSelectorProps, - | "searchable" - | "creatable" - | "getCreateLabel" - | "onCreate" - | "options" - | "getkey" ->; - -const ChipInput: FunctionComponent = ({ ...props }) => { - const { value, onChange } = props; - - const options = useSelectorOptions(value ?? [], (v) => v); +export interface ChipInputProps { + defaultValue?: string[] | undefined; + value?: readonly string[] | null; + label?: string; + onChange?: (value: string[]) => void; +} +const ChipInput: FunctionComponent = ({ + defaultValue, + value, + label, + onChange, +}: ChipInputProps) => { + // TODO: Replace with our own custom implementation instead of just using the + // built-in TagsInput. https://mantine.dev/combobox/?e=MultiSelectCreatable return ( - `Add "${query}"`} - onCreate={(query) => { - onChange?.([...(value ?? []), query]); - return query; - }} - buildOption={(value) => value} - > + v) : []} + onChange={onChange} + clearable + > ); }; diff --git a/frontend/src/components/inputs/DropContent.module.scss b/frontend/src/components/inputs/DropContent.module.scss new file mode 100644 index 000000000..c6c0f848a --- /dev/null +++ b/frontend/src/components/inputs/DropContent.module.scss @@ -0,0 +1,4 @@ +.container { + pointer-events: none; + min-height: 220px; +} diff --git a/frontend/src/components/inputs/DropContent.tsx b/frontend/src/components/inputs/DropContent.tsx index 38556220d..c4bcf2877 100644 --- a/frontend/src/components/inputs/DropContent.tsx +++ b/frontend/src/components/inputs/DropContent.tsx @@ -1,27 +1,17 @@ +import { FunctionComponent } from "react"; +import { Group, Stack, Text } from "@mantine/core"; +import { Dropzone } from "@mantine/dropzone"; import { faArrowUp, faFileCirclePlus, faXmark, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Group, Stack, Text, createStyles } from "@mantine/core"; -import { Dropzone } from "@mantine/dropzone"; -import { FunctionComponent } from "react"; - -const useStyle = createStyles((theme) => { - return { - container: { - pointerEvents: "none", - minHeight: 220, - }, - }; -}); +import styles from "./DropContent.module.scss"; export const DropContent: FunctionComponent = () => { - const { classes } = useStyle(); - return ( - + @@ -31,9 +21,9 @@ export const DropContent: FunctionComponent = () => { - + Upload Subtitles - + Attach as many files as you like, you will need to select file metadata before uploading diff --git a/frontend/src/components/inputs/FileBrowser.tsx b/frontend/src/components/inputs/FileBrowser.tsx index ce57a4938..bba66a66e 100644 --- a/frontend/src/components/inputs/FileBrowser.tsx +++ b/frontend/src/components/inputs/FileBrowser.tsx @@ -1,8 +1,13 @@ -import { useFileSystem } from "@/apis/hooks"; +import { FunctionComponent, useEffect, useMemo, useRef, useState } from "react"; +import { + Autocomplete, + AutocompleteProps, + ComboboxItem, + OptionsFilter, +} from "@mantine/core"; import { faFolder } from "@fortawesome/free-regular-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Autocomplete, AutocompleteProps } from "@mantine/core"; -import { FunctionComponent, useEffect, useMemo, useRef, useState } from "react"; +import { useFileSystem } from "@/apis/hooks"; // TODO: use fortawesome icons const backKey = "⏎ Back"; @@ -75,24 +80,28 @@ export const FileBrowser: FunctionComponent = ({ const ref = useRef(null); + const optionsFilter: OptionsFilter = ({ options, search }) => { + return (options as ComboboxItem[]).filter((option) => { + if (search === backKey) { + return true; + } + + return option.value.includes(search); + }); + }; + return ( } + leftSection={} placeholder="Click to start" data={data} value={value} // Temporary solution of infinite dropdown items, fix later limit={NaN} maxDropdownHeight={240} - filter={(value, item) => { - if (item.value === backKey) { - return true; - } else { - return item.value.includes(value); - } - }} + filter={optionsFilter} onChange={(val) => { if (val !== backKey) { setValue(val); diff --git a/frontend/src/components/inputs/Selector.test.tsx b/frontend/src/components/inputs/Selector.test.tsx index a7b6cfb85..a28772a2d 100644 --- a/frontend/src/components/inputs/Selector.test.tsx +++ b/frontend/src/components/inputs/Selector.test.tsx @@ -1,6 +1,6 @@ -import { rawRender, screen } from "@/tests"; import userEvent from "@testing-library/user-event"; import { describe, it, vitest } from "vitest"; +import { render, screen } from "@/tests"; import { Selector, SelectorOption } from "./Selector"; const selectorName = "Test Selections"; @@ -18,20 +18,17 @@ const testOptions: SelectorOption[] = [ describe("Selector", () => { describe("options", () => { it("should work with the SelectorOption", () => { - rawRender( - , - ); + render(); - // TODO: selectorName - expect(screen.getByRole("searchbox")).toBeDefined(); + testOptions.forEach((o) => { + expect(screen.getByText(o.label)).toBeDefined(); + }); }); it("should display when clicked", async () => { - rawRender( - , - ); + render(); - const element = screen.getByRole("searchbox"); + const element = screen.getByTestId("input-selector"); await userEvent.click(element); @@ -44,7 +41,7 @@ describe("Selector", () => { it("shouldn't show default value", async () => { const option = testOptions[0]; - rawRender( + render( { it("shouldn't show value", async () => { const option = testOptions[0]; - rawRender( + render( { const mockedFn = vitest.fn((value: string | null) => { expect(value).toEqual(clickedOption.value); }); - rawRender( + render( { >, ); - const element = screen.getByRole("searchbox"); + const element = screen.getByTestId("input-selector"); await userEvent.click(element); await userEvent.click(screen.getByText(clickedOption.label)); - expect(mockedFn).toBeCalled(); + expect(mockedFn).toHaveBeenCalled(); }); }); @@ -115,7 +112,7 @@ describe("Selector", () => { const mockedFn = vitest.fn((value: { name: string } | null) => { expect(value).toEqual(clickedOption.value); }); - rawRender( + render( { >, ); - const element = screen.getByRole("searchbox"); + const element = screen.getByTestId("input-selector"); await userEvent.click(element); await userEvent.click(screen.getByText(clickedOption.label)); - expect(mockedFn).toBeCalled(); + expect(mockedFn).toHaveBeenCalled(); }); }); describe("placeholder", () => { it("should show when no selection", () => { const placeholder = "Empty Selection"; - rawRender( + render( = Override< { value: T; label: string; }, - SelectItem + ComboboxItem >; -type SelectItemWithPayload = SelectItem & { +type SelectItemWithPayload = ComboboxItem & { payload: T; }; @@ -34,6 +35,33 @@ function DefaultKeyBuilder(value: T) { } } +export interface GroupedSelectorOptions { + group: string; + items: SelectorOption[]; +} + +export type GroupedSelectorProps = Override< + { + options: ComboboxItemGroup[]; + getkey?: (value: T) => string; + }, + Omit +>; + +export function GroupedSelector({ + options, + ...select +}: GroupedSelectorProps) { + return ( + + ); +} + export type SelectorProps = Override< { value?: T | null; @@ -84,7 +112,7 @@ export function Selector({ }, [defaultValue, keyRef]); const wrappedOnChange = useCallback( - (value: string) => { + (value: string | null) => { const payload = data.find((v) => v.value === value)?.payload ?? null; onChange?.(payload); }, @@ -93,7 +121,8 @@ export function Selector({ return ( ({ () => value && value.map(labelRef.current), [value], ); + const wrappedDefaultValue = useMemo( () => defaultValue && defaultValue.map(labelRef.current), [defaultValue], @@ -168,6 +198,7 @@ export function MultiSelector({ return ( = ({ const { data } = history; - const columns = useMemo[]>( + const addMovieToBlacklist = useMovieAddBlacklist(); + + const columns = useMemo[]>( () => [ { - accessor: "action", - Cell: (row) => ( + id: "action", + cell: ({ + row: { + original: { action }, + }, + }) => ( - + ), }, { - Header: "Language", - accessor: "language", - Cell: ({ value }) => { - if (value) { + header: "Language", + accessorKey: "language", + cell: ({ + row: { + original: { language }, + }, + }) => { + if (language) { return ( - + ); } else { @@ -56,17 +66,20 @@ const MovieHistoryView: FunctionComponent = ({ }, }, { - Header: "Provider", - accessor: "provider", + header: "Provider", + accessorKey: "provider", }, { - Header: "Score", - accessor: "score", + header: "Score", + accessorKey: "score", }, { - accessor: "matches", - Cell: (row) => { - const { matches, dont_matches: dont } = row.row.original; + id: "matches", + cell: ({ + row: { + original: { matches, dont_matches: dont }, + }, + }) => { if (matches.length || dont.length) { return ( = ({ }, }, { - Header: "Date", - accessor: "timestamp", - Cell: ({ value, row }) => { + header: "Date", + accessorKey: "timestamp", + cell: ({ + row: { + original: { timestamp, parsed_timestamp: parsedTimestamp }, + }, + }) => { return ( - - {value} + + {timestamp} ); }, }, { // Actions - accessor: "blacklisted", - Cell: ({ row, value }) => { - const add = useMovieAddBlacklist(); - const { radarrId, provider, subs_id, language, subtitles_path } = - row.original; - + id: "blacklisted", + cell: ({ + row: { + original: { + blacklisted, + radarrId, + provider, + subs_id, + language, + subtitles_path, + }, + }, + }) => { if (subs_id && provider && language) { return ( ({ id: radarrId, form: { @@ -123,7 +147,7 @@ const MovieHistoryView: FunctionComponent = ({ }, }, ], - [], + [addMovieToBlacklist], ); return ( @@ -153,24 +177,34 @@ const EpisodeHistoryView: FunctionComponent = ({ const { data } = history; - const columns = useMemo[]>( + const addEpisodeToBlacklist = useEpisodeAddBlacklist(); + + const columns = useMemo[]>( () => [ { - accessor: "action", - Cell: (row) => ( + id: "action", + cell: ({ + row: { + original: { action }, + }, + }) => ( - + ), }, { - Header: "Language", - accessor: "language", - Cell: ({ value }) => { - if (value) { + header: "Language", + accessorKey: "language", + cell: ({ + row: { + original: { language }, + }, + }) => { + if (language) { return ( - + ); } else { @@ -179,16 +213,16 @@ const EpisodeHistoryView: FunctionComponent = ({ }, }, { - Header: "Provider", - accessor: "provider", + header: "Provider", + accessorKey: "provider", }, { - Header: "Score", - accessor: "score", + header: "Score", + accessorKey: "score", }, { - accessor: "matches", - Cell: (row) => { + id: "matches", + cell: (row) => { const { matches, dont_matches: dont } = row.row.original; if (matches.length || dont.length) { return ( @@ -204,21 +238,29 @@ const EpisodeHistoryView: FunctionComponent = ({ }, }, { - Header: "Date", - accessor: "timestamp", - Cell: ({ row, value }) => { + header: "Date", + accessorKey: "timestamp", + cell: ({ + row: { + original: { timestamp, parsed_timestamp: parsedTimestamp }, + }, + }) => { return ( - - {value} + + {timestamp} ); }, }, { - accessor: "description", - Cell: ({ value }) => { + id: "description", + cell: ({ + row: { + original: { description }, + }, + }) => { return ( - + ); @@ -226,25 +268,27 @@ const EpisodeHistoryView: FunctionComponent = ({ }, { // Actions - accessor: "blacklisted", - Cell: ({ row, value }) => { - const { - sonarrEpisodeId, - sonarrSeriesId, - provider, - subs_id, - language, - subtitles_path, - } = row.original; - const add = useEpisodeAddBlacklist(); - + id: "blacklisted", + cell: ({ + row: { + original: { + blacklisted, + sonarrEpisodeId, + sonarrSeriesId, + provider, + subs_id, + language, + subtitles_path, + }, + }, + }) => { if (subs_id && provider && language) { return ( ({ seriesId: sonarrSeriesId, episodeId: sonarrEpisodeId, @@ -263,12 +307,13 @@ const EpisodeHistoryView: FunctionComponent = ({ }, }, ], - [], + [addEpisodeToBlacklist], ); return ( { download: (item: T, result: SearchResultType) => Promise; - query: ( - id?: number, - ) => UseQueryResult; + query: (id?: number) => UseQueryResult; item: T; } @@ -50,27 +48,67 @@ function ManualSearchView(props: Props) { const search = useCallback(() => { setSearchStarted(true); - results.refetch(); + + void results.refetch(); }, [results]); - const columns = useMemo[]>( + const ReleaseInfoCell = React.memo( + ({ releaseInfo }: { releaseInfo: string[] }) => { + const [open, setOpen] = useState(false); + + const items = useMemo( + () => releaseInfo.slice(1).map((v, idx) => {v}), + [releaseInfo], + ); + + if (releaseInfo.length === 0) { + return Cannot get release info; + } + + return ( + setOpen((o) => !o)}> + + {releaseInfo[0]} + {releaseInfo.length > 1 && ( + + )} + + + <>{items}> + + + ); + }, + ); + + const columns = useMemo[]>( () => [ { - Header: "Score", - accessor: "score", - Cell: ({ value }) => { - const { classes } = useTableStyles(); - return {value}%; + header: "Score", + accessorKey: "score", + cell: ({ + row: { + original: { score }, + }, + }) => { + return {score}%; }, }, { - Header: "Language", - accessor: "language", - Cell: ({ row: { original }, value }) => { + header: "Language", + accessorKey: "language", + cell: ({ + row: { + original: { language, hearing_impaired: hi, forced }, + }, + }) => { const lang: Language.Info = { - code2: value, - hi: original.hearing_impaired === "True", - forced: original.forced === "True", + code2: language, + hi: hi === "True", + forced: forced === "True", name: "", }; return ( @@ -81,16 +119,19 @@ function ManualSearchView(props: Props) { }, }, { - Header: "Provider", - accessor: "provider", - Cell: (row) => { - const { classes } = useTableStyles(); - const value = row.value; - const { url } = row.row.original; + header: "Provider", + accessorKey: "provider", + cell: ({ + row: { + original: { provider, url }, + }, + }) => { + const value = provider; + if (url) { return ( (props: Props) { }, }, { - Header: "Release", - accessor: "release_info", - Cell: ({ value }) => { - const { classes } = useTableStyles(); - const [open, setOpen] = useState(false); - - const items = useMemo( - () => value.slice(1).map((v, idx) => {v}), - [value], - ); - - if (value.length === 0) { - return Cannot get release info; - } - - return ( - setOpen((o) => !o)}> - - {value[0]} - {value.length > 1 && ( - - )} - - - <>{items}> - - - ); + header: "Release", + accessorKey: "release_info", + cell: ({ + row: { + original: { release_info: releaseInfo }, + }, + }) => { + return ; }, }, { - Header: "Uploader", - accessor: "uploader", - Cell: ({ value }) => { - const { classes } = useTableStyles(); - return {value ?? "-"}; + header: "Uploader", + accessorKey: "uploader", + cell: ({ + row: { + original: { uploader }, + }, + }) => { + return {uploader ?? "-"}; }, }, { - Header: "Match", - accessor: "matches", - Cell: (row) => { + header: "Match", + accessorKey: "matches", + cell: (row) => { const { matches, dont_matches: dont } = row.row.original; return ( (props: Props) { }, }, { - Header: "Get", - accessor: "subtitle", - Cell: ({ row }) => { + header: "Get", + accessorKey: "subtitle", + cell: ({ row }) => { const result = row.original; return ( { if (!item) return; @@ -187,7 +207,7 @@ function ManualSearchView(props: Props) { }, }, ], - [download, item], + [download, item, ReleaseInfoCell], ); const bSceneNameAvailable = diff --git a/frontend/src/components/modals/SubtitleToolsModal.tsx b/frontend/src/components/modals/SubtitleToolsModal.tsx index 2ba99ec73..dca20d159 100644 --- a/frontend/src/components/modals/SubtitleToolsModal.tsx +++ b/frontend/src/components/modals/SubtitleToolsModal.tsx @@ -1,12 +1,19 @@ +import { FunctionComponent, useMemo, useState } from "react"; +import { + Badge, + Button, + Checkbox, + Divider, + Group, + Stack, + Text, +} from "@mantine/core"; +import { ColumnDef } from "@tanstack/react-table"; import Language from "@/components/bazarr/Language"; import SubtitleToolsMenu from "@/components/SubtitleToolsMenu"; -import { SimpleTable } from "@/components/tables"; -import { useCustomSelection } from "@/components/tables/plugins"; +import SimpleTable from "@/components/tables/SimpleTable"; import { withModal } from "@/modules/modals"; import { isMovie } from "@/utilities"; -import { Badge, Button, Divider, Group, Stack, Text } from "@mantine/core"; -import { FunctionComponent, useMemo, useState } from "react"; -import { Column, useRowSelect } from "react-table"; type SupportType = Item.Episode | Item.Movie; @@ -35,24 +42,53 @@ const SubtitleToolView: FunctionComponent = ({ }) => { const [selections, setSelections] = useState([]); - const columns: Column[] = useMemo[]>( + const columns = useMemo[]>( () => [ { - Header: "Language", - accessor: "raw_language", - Cell: ({ value }) => ( + id: "selection", + header: ({ table }) => { + return ( + + ); + }, + cell: ({ row: { index, getIsSelected, getToggleSelectedHandler } }) => { + return ( + + ); + }, + }, + { + header: "Language", + accessorKey: "raw_language", + cell: ({ + row: { + original: { raw_language: rawLanguage }, + }, + }) => ( - + ), }, { id: "file", - Header: "File", - accessor: "path", - Cell: ({ value }) => { - const path = value; - + header: "File", + accessorKey: "path", + cell: ({ + row: { + original: { path }, + }, + }) => { let idx = path.lastIndexOf("/"); if (idx === -1) { @@ -94,16 +130,15 @@ const SubtitleToolView: FunctionComponent = ({ [payload], ); - const plugins = [useRowSelect, useCustomSelection]; - return ( CanSelectSubtitle(row.original)} + onRowSelectionChanged={(rows) => + setSelections(rows.map((r) => r.original)) + } columns={columns} - onSelect={setSelections} - canSelect={CanSelectSubtitle} data={data} > diff --git a/frontend/src/components/tables/BaseTable.module.scss b/frontend/src/components/tables/BaseTable.module.scss new file mode 100644 index 000000000..e1e1eff0b --- /dev/null +++ b/frontend/src/components/tables/BaseTable.module.scss @@ -0,0 +1,9 @@ +.container { + display: block; + max-width: 100%; + overflow-x: auto; +} + +.table { + border-collapse: collapse; +} diff --git a/frontend/src/components/tables/BaseTable.tsx b/frontend/src/components/tables/BaseTable.tsx index 6ec49e61a..b5a867b14 100644 --- a/frontend/src/components/tables/BaseTable.tsx +++ b/frontend/src/components/tables/BaseTable.tsx @@ -1,10 +1,17 @@ +import React, { ReactNode, useMemo } from "react"; +import { Box, Skeleton, Table, Text } from "@mantine/core"; +import { + flexRender, + Header, + Row, + Table as TableInstance, +} from "@tanstack/react-table"; import { useIsLoading } from "@/contexts"; import { usePageSize } from "@/utilities/storage"; -import { Box, createStyles, Skeleton, Table, Text } from "@mantine/core"; -import { ReactNode, useMemo } from "react"; -import { HeaderGroup, Row, TableInstance } from "react-table"; +import styles from "@/components/tables/BaseTable.module.scss"; -export type BaseTableProps = TableInstance & { +export type BaseTableProps = { + instance: TableInstance; tableStyles?: TableStyleProps; }; @@ -14,116 +21,92 @@ export interface TableStyleProps { placeholder?: number; hideHeader?: boolean; fixHeader?: boolean; - headersRenderer?: (headers: HeaderGroup[]) => JSX.Element[]; - rowRenderer?: (row: Row) => Nullable; + headersRenderer?: (headers: Header[]) => React.JSX.Element[]; + rowRenderer?: (row: Row) => Nullable; } -const useStyles = createStyles((theme) => { - return { - container: { - display: "block", - maxWidth: "100%", - overflowX: "auto", - }, - table: { - borderCollapse: "collapse", - }, - header: {}, - }; -}); - function DefaultHeaderRenderer( - headers: HeaderGroup[], -): JSX.Element[] { - return headers.map((col) => ( - - {col.render("Header")} - + headers: Header[], +): React.JSX.Element[] { + return headers.map((header) => ( + + {flexRender(header.column.columnDef.header, header.getContext())} + )); } -function DefaultRowRenderer(row: Row): JSX.Element | null { +function DefaultRowRenderer( + row: Row, +): React.JSX.Element | null { return ( - - {row.cells.map((cell) => ( - {cell.render("Cell")} + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} - + ); } export default function BaseTable(props: BaseTableProps) { - const { - headerGroups, - rows: tableRows, - page: tablePages, - prepareRow, - getTableProps, - getTableBodyProps, - tableStyles, - } = props; + const { instance, tableStyles } = props; const headersRenderer = tableStyles?.headersRenderer ?? DefaultHeaderRenderer; const rowRenderer = tableStyles?.rowRenderer ?? DefaultRowRenderer; - const { classes } = useStyles(); - const colCount = useMemo(() => { - return headerGroups.reduce( - (prev, curr) => (curr.headers.length > prev ? curr.headers.length : prev), - 0, - ); - }, [headerGroups]); + return instance + .getHeaderGroups() + .reduce( + (prev, curr) => + curr.headers.length > prev ? curr.headers.length : prev, + 0, + ); + }, [instance]); - // Switch to usePagination plugin if enabled - const rows = tablePages ?? tableRows; - - const empty = rows.length === 0; + const empty = instance.getRowCount() === 0; const pageSize = usePageSize(); const isLoading = useIsLoading(); let body: ReactNode; + if (isLoading) { body = Array(tableStyles?.placeholder ?? pageSize) .fill(0) .map((_, i) => ( - - + + - - + + )); } else if (empty && tableStyles?.emptyText) { body = ( - - - {tableStyles.emptyText} - - + + + {tableStyles.emptyText} + + ); } else { - body = rows.map((row) => { - prepareRow(row); + body = instance.getRowModel().rows.map((row) => { return rowRenderer(row); }); } return ( - - - - {headerGroups.map((headerGroup) => ( - + + + + {instance.getHeaderGroups().map((headerGroup) => ( + {headersRenderer(headerGroup.headers)} - + ))} - - {body} + + {body} ); diff --git a/frontend/src/components/tables/GroupTable.tsx b/frontend/src/components/tables/GroupTable.tsx index 3a8be3d1b..b14edf3e6 100644 --- a/frontend/src/components/tables/GroupTable.tsx +++ b/frontend/src/components/tables/GroupTable.tsx @@ -1,38 +1,44 @@ +import React, { Fragment } from "react"; +import { Box, Table, Text } from "@mantine/core"; import { faChevronCircleRight } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Box, Text } from "@mantine/core"; import { Cell, - HeaderGroup, + flexRender, + getExpandedRowModel, + getGroupedRowModel, + Header, Row, - useExpanded, - useGroupBy, - useSortBy, -} from "react-table"; -import SimpleTable, { SimpleTableProps } from "./SimpleTable"; +} from "@tanstack/react-table"; +import SimpleTable, { SimpleTableProps } from "@/components/tables/SimpleTable"; -function renderCell(cell: Cell, row: Row) { - if (cell.isGrouped) { +function renderCell( + cell: Cell, + row: Row, +) { + if (cell.getIsGrouped()) { return ( - {cell.render("Cell")} + {flexRender(cell.column.columnDef.cell, cell.getContext())} ); - } else if (row.canExpand || cell.isAggregated) { + } else if (row.getCanExpand() || cell.getIsAggregated()) { return null; } else { - return cell.render("Cell"); + return flexRender(cell.column.columnDef.cell, cell.getContext()); } } function renderRow(row: Row) { - if (row.canExpand) { - const cell = row.cells.find((cell) => cell.isGrouped); + if (row.getCanExpand()) { + const cell = row.getVisibleCells().find((cell) => cell.getIsGrouped()); + if (cell) { - const rotation = row.isExpanded ? 90 : undefined; + const rotation = row.getIsExpanded() ? 90 : undefined; + return ( - - - - {cell.render("Cell")} + + + row.toggleExpanded()}> + {flexRender(cell.column.columnDef.cell, cell.getContext())} (row: Row) { > - - + + ); } else { return null; } } else { return ( - - {row.cells - .filter((cell) => !cell.isPlaceholder) + + {row + .getVisibleCells() + .filter((cell) => !cell.getIsPlaceholder()) .map((cell) => ( - {renderCell(cell, row)} + {renderCell(cell, row)} ))} - + ); } } function renderHeaders( - headers: HeaderGroup[], -): JSX.Element[] { - return headers - .filter((col) => !col.isGrouped) - .map((col) => {col.render("Header")}); + headers: Header[], +): React.JSX.Element[] { + return headers.map((header) => { + if (header.column.getIsGrouped()) { + return ; + } + + return ( + + {flexRender(header.column.columnDef.header, header.getContext())} + + ); + }); } type Props = Omit< SimpleTableProps, - "plugins" | "headersRenderer" | "rowRenderer" + "headersRenderer" | "rowRenderer" >; -const plugins = [useGroupBy, useSortBy, useExpanded]; - function GroupTable(props: Props) { return ( ); diff --git a/frontend/src/components/tables/PageControl.tsx b/frontend/src/components/tables/PageControl.tsx index 0767593de..bcdf290e3 100644 --- a/frontend/src/components/tables/PageControl.tsx +++ b/frontend/src/components/tables/PageControl.tsx @@ -1,6 +1,7 @@ -import { useIsLoading } from "@/contexts"; -import { Group, Pagination, Text } from "@mantine/core"; import { FunctionComponent, useEffect } from "react"; +import { Group, Pagination, Text } from "@mantine/core"; +import { useIsLoading } from "@/contexts"; + interface Props { count: number; index: number; @@ -28,7 +29,7 @@ const PageControl: FunctionComponent = ({ }, [total, goto]); return ( - + Show {start} to {end} of {total} entries diff --git a/frontend/src/components/tables/PageTable.tsx b/frontend/src/components/tables/PageTable.tsx index 4f64fe7b8..476ff2c2b 100644 --- a/frontend/src/components/tables/PageTable.tsx +++ b/frontend/src/components/tables/PageTable.tsx @@ -1,55 +1,62 @@ +import { MutableRefObject, useEffect } from "react"; +import { + getCoreRowModel, + getPaginationRowModel, + Table, + TableOptions, + useReactTable, +} from "@tanstack/react-table"; +import BaseTable, { TableStyleProps } from "@/components/tables/BaseTable"; import { ScrollToTop } from "@/utilities"; import { usePageSize } from "@/utilities/storage"; -import { useEffect } from "react"; -import { usePagination, useTable } from "react-table"; -import BaseTable from "./BaseTable"; import PageControl from "./PageControl"; -import { SimpleTableProps } from "./SimpleTable"; -import { useDefaultSettings } from "./plugins"; -type Props = SimpleTableProps & { +type Props = Omit, "getCoreRowModel"> & { + instanceRef?: MutableRefObject | null>; + tableStyles?: TableStyleProps; autoScroll?: boolean; }; -const tablePlugins = [useDefaultSettings, usePagination]; - export default function PageTable