···13131414class ForceReactivation
1515{
1616+ const INACTIVE = 'inactive';
1617 const INACTIVE_DIFFERENT_COUNTRY = 'inactive_different_country';
17181819 private $country;
···27282829 $this->country = request_country($this->request);
29303030- if ($this->user->isInactive() && $this->user->country_acronym !== $this->country) {
3131- $this->reason = static::INACTIVE_DIFFERENT_COUNTRY;
3131+ if ($this->user->isInactive()) {
3232+ if ($this->user->country_acronym !== $this->country) {
3333+ $this->reason = static::INACTIVE_DIFFERENT_COUNTRY;
3434+ } elseif ($GLOBALS['cfg']['osu']['user']['inactive_force_password_reset']) {
3535+ $this->reason = static::INACTIVE;
3636+ }
3237 }
3338 }
3439···62676368 private function addHistoryNote()
6469 {
6565- if ($this->reason === static::INACTIVE_DIFFERENT_COUNTRY) {
6666- $message = "First login after {$this->user->user_lastvisit->diffInDays()} days from {$this->country}. Forcing password reset.";
6767- }
7070+ $message = match ($this->reason) {
7171+ static::INACTIVE => "First login after {$this->user->user_lastvisit->diffInDays()} days. Forcing password reset.",
7272+ static::INACTIVE_DIFFERENT_COUNTRY => "First login after {$this->user->user_lastvisit->diffInDays()} days from {$this->country}. Forcing password reset.",
7373+ default => null,
7474+ };
68756976 if ($message !== null) {
7077 UserAccountHistory::addNote($this->user, $message);
+7
app/Models/BeatmapPack.php
···117117 ];
118118 if ($this->no_diff_reduction) {
119119 $params['exclude_mods'] = app('mods')->difficultyReductionIds->toArray();
120120+ if ($isLegacy !== true) {
121121+ // the intended meaning of this check is that the scores should not include mods
122122+ // that disqualify them from granting pp.
123123+ // mods are not the only reason why pp might be missing, but it's the best that we have for now.
124124+ // see also: https://github.com/ppy/osu-queue-score-statistics/pull/234
125125+ $params['exclude_without_pp'] = true;
126126+ }
120127 }
121128122129 static $aggName = 'by_beatmap';
+19-13
app/Models/Beatmapset.php
···501501 }
502502503503 $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
504504+ // archive file is gone, nothing to do for now
505505+ if ($statusCode === 302) {
506506+ return false;
507507+ }
504508 if ($statusCode !== 200) {
505509 throw new BeatmapProcessorException('Failed downloading osz: HTTP Error '.$statusCode);
506510 }
507511508508- return new BeatmapsetArchive(get_stream_filename($oszFile));
512512+ try {
513513+ return new BeatmapsetArchive(get_stream_filename($oszFile));
514514+ } catch (BeatmapProcessorException $e) {
515515+ // zip file is broken, nothing to do for now
516516+ return false;
517517+ }
509518 }
510519511520 public function regenerateCovers(array $sizesToRegenerate = null)
···531540532541 if ($backgroundFilename !== false) {
533542 $tmpFile = tmpfile();
534534- $bytesWritten = fwrite($tmpFile, $osz->readFile($backgroundFilename));
535535- fseek($tmpFile, 0); // reset file position cursor, required for storeCover below
543543+ fwrite($tmpFile, $osz->readFile($backgroundFilename));
536544 $backgroundImage = get_stream_filename($tmpFile);
537545 if (!static::isValidBackgroundImage($backgroundImage)) {
538546 return false;
···1481148914821490 public function getDisplayArtist(?User $user)
14831491 {
14841484- $profileCustomization = $user->userProfileCustomization ?? new UserProfileCustomization();
14851485- if ($profileCustomization->beatmapset_title_show_original) {
14861486- return $this->artist_unicode;
14871487- }
14921492+ $profileCustomization = $user->userProfileCustomization ?? UserProfileCustomization::DEFAULTS;
1488149314891489- return $this->artist;
14941494+ return $profileCustomization['beatmapset_title_show_original']
14951495+ ? $this->artist_unicode
14961496+ : $this->artist;
14901497 }
1491149814921499 public function getDisplayTitle(?User $user)
14931500 {
14941494- $profileCustomization = $user->userProfileCustomization ?? new UserProfileCustomization();
14951495- if ($profileCustomization->beatmapset_title_show_original) {
14961496- return $this->title_unicode;
14971497- }
15011501+ $profileCustomization = $user->userProfileCustomization ?? UserProfileCustomization::DEFAULTS;
1498150214991499- return $this->title;
15031503+ return $profileCustomization['beatmapset_title_show_original']
15041504+ ? $this->title_unicode
15051505+ : $this->title;
15001506 }
1501150715021508 public function freshHype()
···11+<?php
22+33+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
44+// See the LICENCE file in the repository root for full licence text.
55+66+declare(strict_types=1);
77+88+use Illuminate\Database\Migrations\Migration;
99+use Illuminate\Database\Schema\Blueprint;
1010+use Illuminate\Support\Facades\Schema;
1111+1212+return new class extends Migration
1313+{
1414+ public function up(): void
1515+ {
1616+ Schema::table('score_pins', function (Blueprint $table) {
1717+ $table->unsignedBigInteger('new_score_id')->after('score_id')->nullable(true);
1818+ });
1919+ }
2020+2121+ public function down(): void
2222+ {
2323+ Schema::table('score_pins', function (Blueprint $table) {
2424+ $table->dropColumn('new_score_id');
2525+ });
2626+ }
2727+};
···11+<?php
22+33+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
44+// See the LICENCE file in the repository root for full licence text.
55+66+declare(strict_types=1);
77+88+use Illuminate\Database\Migrations\Migration;
99+use Illuminate\Database\Schema\Blueprint;
1010+use Illuminate\Support\Facades\Schema;
1111+1212+return new class extends Migration
1313+{
1414+ public function up(): void
1515+ {
1616+ Schema::create('user_cover_presets', function (Blueprint $table) {
1717+ $table->mediumIncrements('id');
1818+ $table->string('filename')->nullable(true);
1919+ $table->boolean('active')->default(false)->nullable(false);
2020+ $table->timestamps();
2121+ $table->index('active');
2222+ });
2323+ }
2424+2525+ public function down(): void
2626+ {
2727+ Schema::dropIfExists('user_cover_presets');
2828+ }
2929+};
+6-6
docker-compose.yml
···107107 # important to use 127.0.0.1 instead of localhost as mysql starts twice.
108108 # the first time it listens on sockets but isn't actually ready
109109 # see https://github.com/docker-library/mysql/issues/663
110110+ start_interval: 1s
111111+ start_period: 60s
110112 test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
111111- interval: 1s
112113 timeout: 60s
113113- start_period: 60s
114114115115 redis:
116116 command: ['redis-server', '--save', '60', '1', '--loglevel', 'warning']
···118118 ports:
119119 - "${REDIS_EXTERNAL_PORT:-127.0.0.1:6379}:6379"
120120 healthcheck:
121121+ start_interval: 1s
122122+ start_period: 60s
121123 test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
122122- interval: 1s
123124 timeout: 60s
124124- start_period: 60s
125125 volumes:
126126 - redis:/data
127127···138138 ES_JAVA_OPTS: "-Xms512m -Xmx512m" # less OOM on default settings.
139139 ingest.geoip.downloader.enabled: false
140140 healthcheck:
141141+ start_interval: 1s
142142+ start_period: 60s
141143 test: curl -s http://localhost:9200/_cluster/health?wait_for_status=yellow >/dev/null || exit 1
142142- interval: 1s
143144 timeout: 60s
144144- start_period: 60s
145145146146 nginx:
147147 image: nginx:latest
···11+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
22+// See the LICENCE file in the repository root for full licence text.
33+44+.user-cover-preset-replace {
55+ display: grid;
66+ gap: 10px;
77+ padding-top: 10px;
88+ border-top: 1px solid hsl(var(--hsl-b1));
99+1010+ &__input {
1111+ width: 100%;
1212+ }
1313+}
+38
resources/css/bem/user-cover-preset-table.less
···11+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
22+// See the LICENCE file in the repository root for full licence text.
33+44+.user-cover-preset-table {
55+ display: grid;
66+ grid-template-columns: auto 150px 1fr;
77+88+ &__image {
99+ width: 100%;
1010+ }
1111+1212+ &__toolbar {
1313+ grid-column-start: 2;
1414+ grid-column-end: 4;
1515+ }
1616+1717+ &__row {
1818+ padding: 10px;
1919+ gap: 10px;
2020+2121+ display: grid;
2222+ grid-column: ~"1 / 4";
2323+ grid-template-columns: subgrid;
2424+2525+ &--item {
2626+ background: var(--row-bg);
2727+ --row-bg: hsl(var(--hsl-b4));
2828+2929+ &:hover {
3030+ background: hsl(var(--hsl-b2));
3131+ }
3232+3333+ &:nth-child(even) {
3434+ --row-bg: hsl(var(--hsl-b3));
3535+ }
3636+ }
3737+ }
3838+}
···11-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
22-// See the LICENCE file in the repository root for full licence text.
33-44-import UserJson from 'interfaces/user-json';
55-import * as React from 'react';
66-import { UserCard } from './user-card';
77-88-interface Props {
99- user: UserJson | null;
1010-}
1111-1212-interface State {
1313- user?: UserJson | null;
1414-}
1515-1616-/**
1717- * This component's job shims UserCard for store-supporter-tag to update UserCard's props.
1818- */
1919-export class UserCardStore extends React.PureComponent<Props, State> {
2020- state: Readonly<State> = { user: this.props.user };
2121-2222- componentDidMount() {
2323- $.subscribe('store-supporter-tag:update-user', this.setUser);
2424- }
2525-2626- componentWillUnmount() {
2727- $.unsubscribe('store-supporter-tag:update-user', this.setUser);
2828- }
2929-3030- render() {
3131- return <UserCard user={this.state.user} />;
3232- }
3333-3434- setUser = (event: JQuery.Event, user?: UserJson) => {
3535- this.setState({ user });
3636- };
3737-}
+3-3
resources/js/core-legacy/nav2.coffee
···11# Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
22# See the LICENCE file in the repository root for full licence text.
3344-import { blackoutHide, blackoutShow } from 'utils/blackout'
44+import { blackoutToggle } from 'utils/blackout'
55import { fadeToggle } from 'utils/fade'
6677export default class Nav2
···50505151 if @showingMobileNav
5252 document.body.classList.add('js-nav2--active')
5353- blackoutShow()
5353+ blackoutToggle(this, true)
5454 else if previousTree.indexOf('mobile-menu') != -1
5555- blackoutHide()
5555+ blackoutToggle(this, false)
5656 Timeout.set 0, =>
5757 $(@clickMenu.menu('mobile-menu')).finish().slideUp 150, =>
5858 # use actual state instead of always removing the class in case
···11-# Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
22-# See the LICENCE file in the repository root for full licence text.
33-44-import { fadeOut } from 'utils/fade'
55-66-export default class TwitchPlayer
77- constructor: (@turbolinksReload) ->
88- @playerDivs = document.getElementsByClassName('js-twitch-player')
99-1010- addEventListener 'turbolinks:load', @startAll
1111-1212-1313- initializeEmbed: =>
1414- @turbolinksReload
1515- .load 'https://player.twitch.tv/js/embed/v1.js'
1616- ?.then @startAll
1717-1818-1919- startAll: =>
2020- return if @playerDivs.length == 0
2121-2222- if !Twitch?
2323- @initializeEmbed()
2424- else
2525- @start(div) for div in @playerDivs
2626-2727-2828- start: (div) =>
2929- return if div.dataset.twitchPlayerStarted
3030-3131- div.dataset.twitchPlayerStarted = true
3232- options =
3333- width: '100%'
3434- height: '100%'
3535- channel: div.dataset.channel
3636-3737- player = new Twitch.Player(div.id, options)
3838- player.addEventListener Twitch.Player.PLAY, => @openPlayer(div)
3939-4040-4141- noCookieDiv: (playerDivId) =>
4242- document.querySelector(".js-twitch-player--no-cookie[data-player-id='#{playerDivId}']")
4343-4444-4545- openPlayer: (div) =>
4646- return unless div.classList.contains 'hidden'
4747-4848- div.classList.remove 'hidden'
4949- fadeOut @noCookieDiv(div.id)
+12
resources/js/core/twitch-embed-player.d.ts
···11+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
22+// See the LICENCE file in the repository root for full licence text.
33+44+declare module 'twitch-embed-player' {
55+ // (2024-03-26) see https://dev.twitch.tv/docs/embed/video-and-clips/ for all options.
66+ export default class TwitchEmbedPlayer {
77+ static PLAY: string;
88+99+ constructor (id: string, options: Record<string, unknown>);
1010+ addEventListener(action: string, callback: () => void): void;
1111+ }
1212+}
+69
resources/js/core/twitch-player.tsx
···11+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
22+// See the LICENCE file in the repository root for full licence text.
33+44+import type TwitchEmbedPlayer from 'twitch-embed-player';
55+import { fadeOut } from 'utils/fade';
66+import TurbolinksReload from './turbolinks-reload';
77+88+declare global {
99+ interface Window {
1010+ Twitch?: {
1111+ Embed: unknown; // unused
1212+ Player: typeof TwitchEmbedPlayer;
1313+ };
1414+ }
1515+}
1616+1717+export default class TwitchPlayer {
1818+ private readonly playerDivs = document.getElementsByClassName('js-twitch-player');
1919+2020+ constructor(private readonly turbolinksReload: TurbolinksReload) {
2121+ document.addEventListener('turbolinks:load', this.startAll);
2222+ }
2323+2424+ initializeEmbed() {
2525+ this.turbolinksReload
2626+ .load('https://player.twitch.tv/js/embed/v1.js')
2727+ ?.then(this.startAll);
2828+ }
2929+3030+ noCookieDiv(playerDivId: string) {
3131+ return document.querySelector<HTMLElement>(`.js-twitch-player--no-cookie[data-player-id='${playerDivId}']`);
3232+ }
3333+3434+ openPlayer(div: HTMLElement) {
3535+ if (!div.classList.contains('hidden')) return;
3636+3737+ div.classList.remove('hidden');
3838+ fadeOut(this.noCookieDiv(div.id));
3939+ }
4040+4141+ start(div: HTMLElement) {
4242+ if (window.Twitch == null
4343+ || div.dataset.twitchPlayerStarted === 'true') return;
4444+4545+ div.dataset.twitchPlayerStarted = 'true';
4646+ const options = {
4747+ channel: div.dataset.channel,
4848+ height: '100%',
4949+ width: '100%',
5050+ };
5151+5252+ const player = new window.Twitch.Player(div.id, options);
5353+ player.addEventListener(window.Twitch.Player.PLAY, () => this.openPlayer(div));
5454+ }
5555+5656+ startAll = () => {
5757+ if (this.playerDivs.length === 0) return;
5858+5959+ if (window.Twitch == null) {
6060+ this.initializeEmbed();
6161+ } else {
6262+ for (const div of this.playerDivs) {
6363+ if (div instanceof HTMLElement) {
6464+ this.start(div);
6565+ }
6666+ }
6767+ }
6868+ };
6969+}
···11+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
22+// See the LICENCE file in the repository root for full licence text.
33+44+import UserCoverPresetBatchActivate from 'user-cover-preset-batch-activate';
55+66+new UserCoverPresetBatchActivate();
-4
resources/js/main.coffee
···3232import Search from 'core-legacy/search'
3333import StickyFooter from 'core-legacy/sticky-footer'
3434import { StoreCheckout } from 'core-legacy/store-checkout'
3535-import StoreSupporterTag from 'core-legacy/store-supporter-tag'
3635import SyncHeight from 'core-legacy/sync-height'
3736import TooltipDefault from 'core-legacy/tooltip-default'
3838-import TwitchPlayer from 'core-legacy/twitch-player'
3937import { hideLoadingOverlay, showLoadingOverlay } from 'utils/loading-overlay'
4038import { navigate } from 'utils/turbolinks'
4139···61596260$(document).on 'turbolinks:load', ->
6361 BeatmapPack.initialize()
6464- StoreSupporterTag.initialize()
6562 StoreCheckout.initialize()
66636764# ensure currentUser is updated early enough.
···9895window.forumTopicPostJump ?= new ForumTopicPostJump(window.forum)
9996window.forumTopicReply ?= new ForumTopicReply(bbcodePreview: window.bbcodePreview, forum: window.forum, stickyFooter: window.stickyFooter)
10097window.nav2 ?= new Nav2(osuCore.clickMenu)
101101-window.twitchPlayer ?= new TwitchPlayer(osuCore.turbolinksReload)
1029810399104100$(document).on 'change', '.js-url-selector', (e) ->
+4
resources/js/osu-core.ts
···1919import StickyHeader from 'core/sticky-header';
2020import Timeago from 'core/timeago';
2121import TurbolinksReload from 'core/turbolinks-reload';
2222+import TwitchPlayer from 'core/twitch-player';
2223import ScorePins from 'core/user/score-pins';
2324import UserLogin from 'core/user/user-login';
2425import UserLoginObserver from 'core/user/user-login-observer';
···6263 readonly stickyHeader;
6364 readonly timeago;
6465 readonly turbolinksReload;
6666+ readonly twitchPlayer;
6567 readonly userLogin;
6668 readonly userLoginObserver;
6769 readonly userPreferences;
···112114 this.enchant = new Enchant(this.turbolinksReload);
113115 this.osuAudio = new OsuAudio(this.userPreferences);
114116 this.reactTurbolinks = new ReactTurbolinks(this, this.turbolinksReload);
117117+ this.twitchPlayer = new TwitchPlayer(this.turbolinksReload);
118118+115119 this.userLogin = new UserLogin(this.captcha);
116120 // should probably figure how to conditionally or lazy initialize these so they don't all init when not needed.
117121 // TODO: requires dynamic imports to lazy load modules.
+11-5
resources/js/register-components.tsx
···1515import RankingVariantFilter from 'components/ranking-variant-filter';
1616import SpotlightSelectOptions from 'components/spotlight-select-options';
1717import { UserCard } from 'components/user-card';
1818-import { UserCardStore } from 'components/user-card-store';
1918import { startListening, UserCardTooltip } from 'components/user-card-tooltip';
2019import { UserCards } from 'components/user-cards';
2120import { WikiSearch } from 'components/wiki-search';
···2524import QuickSearch from 'quick-search/main';
2625import QuickSearchWorker from 'quick-search/worker';
2726import * as React from 'react';
2727+import StoreSupporterTag from 'store/store-supporter-tag';
2828import { parseJson } from 'utils/json';
2929import { mapBy } from 'utils/map';
3030+import { getInt } from 'utils/math';
30313132function reqJson<T>(input: string|undefined): T {
3233 // This will throw when input is missing and thus parsing empty string.
···106107 <RankingVariantFilter {...parseJson('json-variant-filter')} />
107108));
108109110110+core.reactTurbolinks.register('store-supporter-tag', (container) => {
111111+ const maxMessageLength = getInt(container.dataset.maxMessageLength);
112112+ if (maxMessageLength == null) {
113113+ throw new Error('missing maxMessageLength');
114114+ }
115115+116116+ return <StoreSupporterTag maxMessageLength={maxMessageLength} />;
117117+});
118118+109119core.reactTurbolinks.register('user-card', (container) => (
110120 <UserCard
111121 modifiers={reqJson(container.dataset.modifiers ?? 'null')}
112122 user={container.dataset.isCurrentUser === '1' ? core.currentUser : reqJson(container.dataset.user ?? 'null')}
113123 />
114114-));
115115-116116-core.reactTurbolinks.register('user-card-store', (container) => (
117117- <UserCardStore user={reqJson(container.dataset.user)} />
118124));
119125120126core.reactTurbolinks.register('user-card-tooltip', (container) => (
···11+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
22+// See the LICENCE file in the repository root for full licence text.
33+44+import { route } from 'laroute';
55+import { trans, transChoice } from 'utils/lang';
66+import { popup } from 'utils/popup';
77+import { reloadPage } from 'utils/turbolinks';
88+99+const checkboxSelector = '.js-user-cover-preset-batch-enable--checkbox';
1010+1111+export default class UserCoverPresetBatchActivate {
1212+ private lastSelected: HTMLInputElement | null = null;
1313+ private xhr: JQuery.jqXHR<void> | null = null;
1414+1515+ constructor() {
1616+ $(document)
1717+ .on('click', '.js-user-cover-preset-batch-enable', this.handleEvent)
1818+ .on('turbolinks:before-cache', this.cleanup);
1919+ }
2020+2121+ private applySelected(active: boolean) {
2222+ const ids = [...this.checkboxes()]
2323+ .filter((el) => el.checked)
2424+ .map((el) => el.dataset.id);
2525+ const count = ids.length;
2626+2727+ if (count === 0) {
2828+ popup('no covers selected');
2929+ return;
3030+ }
3131+3232+ if (!confirm(trans('user_cover_presets.index.batch_confirm._', {
3333+ action: trans(`user_cover_presets.index.batch_confirm.${active ? 'enable' : 'disable'}`),
3434+ items: transChoice('user_cover_presets.index.batch_confirm.items', count),
3535+ }))) {
3636+ return;
3737+ }
3838+3939+ this.xhr = $.post(route('user-cover-presets.batch-activate'), { active, ids });
4040+ this.xhr
4141+ .done(() => {
4242+ reloadPage();
4343+ })
4444+ .fail((xhr, status) => {
4545+ if (status !== 'abort') {
4646+ popup('Update failed', 'danger');
4747+ }
4848+ });
4949+ }
5050+5151+ private checkboxes() {
5252+ return document.querySelectorAll<HTMLInputElement>(checkboxSelector);
5353+ }
5454+5555+ private readonly cleanup = () => {
5656+ this.lastSelected = null;
5757+ this.xhr?.abort();
5858+ this.xhr = null;
5959+ };
6060+6161+ private readonly handleEvent = (e: JQuery.ClickEvent<Document, unknown, HTMLElement, HTMLElement>) => {
6262+ const target = e.currentTarget;
6363+6464+ switch (target.dataset.action) {
6565+ case 'disable-selected': return this.applySelected(false);
6666+ case 'enable-selected': return this.applySelected(true);
6767+ case 'select': return this.select(target, e);
6868+ case 'select-all': return this.toggleAll(target as HTMLInputElement);
6969+ }
7070+ };
7171+7272+ private readonly select = (target: HTMLElement, e: JQuery.ClickEvent) => {
7373+ const checkbox = target.querySelector(checkboxSelector);
7474+ if (checkbox instanceof HTMLInputElement) {
7575+ if ((e.originalEvent?.shiftKey ?? false) && this.lastSelected != null) {
7676+ const checked = this.lastSelected.checked;
7777+ let started = false;
7878+ for (const el of this.checkboxes()) {
7979+ if (el === checkbox) {
8080+ el.checked = checked;
8181+ }
8282+ if (el === this.lastSelected || el === checkbox) {
8383+ if (started) {
8484+ break;
8585+ } else {
8686+ started = true;
8787+ }
8888+ continue;
8989+ }
9090+ if (started) {
9191+ el.checked = checked;
9292+ }
9393+ }
9494+ }
9595+ this.lastSelected = checkbox;
9696+ }
9797+ this.syncToggleState();
9898+ };
9999+100100+ private selectAllCheckbox() {
101101+ const ret = document.querySelector('.js-user-cover-preset-batch-enable--select-all');
102102+ if (!(ret instanceof HTMLInputElement)) {
103103+ throw new Error('select all checkbox element is not HTMLInputElement');
104104+ }
105105+106106+ return ret;
107107+ }
108108+109109+ private syncToggleState() {
110110+ const selectAllCheckbox = this.selectAllCheckbox();
111111+ let state: boolean | null = null;
112112+ for (const el of this.checkboxes()) {
113113+ if (state == null) {
114114+ selectAllCheckbox.checked = state = el.checked;
115115+ selectAllCheckbox.dataset.indeterminate = 'false';
116116+ } else {
117117+ if (state !== el.checked) {
118118+ selectAllCheckbox.dataset.indeterminate = 'true';
119119+ break;
120120+ }
121121+ }
122122+ }
123123+ }
124124+125125+ private readonly toggleAll = (target: HTMLInputElement) => {
126126+ const checked = target.checked;
127127+ for (const el of this.checkboxes()) {
128128+ el.checked = checked;
129129+ }
130130+ target.dataset.indeterminate = 'false';
131131+ };
132132+}
+12-16
resources/js/utils/blackout.ts
···3344import { fadeToggle } from './fade';
5566-export function blackoutHide() {
77- blackoutToggle(false);
88-}
99-1010-export function blackoutShow() {
1111- blackoutToggle(true);
1212-}
1313-1414-export function blackoutToggle(state: boolean, opacity?: number) {
1515- const el = window.newBody?.querySelector('.js-blackout');
66+const elements = new Set<unknown>();
1671717- if (el instanceof HTMLElement) {
1818- el.style.opacity = !state || opacity == null ? '' : String(opacity);
1919- fadeToggle(el, state);
88+export function blackoutToggle(element: unknown, state: boolean) {
99+ if (state) {
1010+ elements.add(element);
1111+ } else {
1212+ elements.delete(element);
2013 }
1414+1515+ fadeToggle(
1616+ window.newBody?.querySelector('.js-blackout'),
1717+ blackoutVisible(),
1818+ );
2119}
22202321export function blackoutVisible() {
2424- const el = document.querySelector('.js-blackout');
2525-2626- return el instanceof HTMLElement && el.style.opacity !== '';
2222+ return elements.size > 0;
2723}
+1-1
resources/js/utils/json.ts
···8989 * @param id id of the element to store to. Contents of an existing HTMLScriptElement will be overriden.
9090 * @param object state to store.
9191 */
9292-export function storeJson(id: string, object: unknown) {
9292+export function storeJson<T = unknown>(id: string, object: T) {
9393 const json = JSON.stringify(object);
9494 const maybeElement = document.getElementById(id);
9595
+12-2
resources/js/utils/store-cart.ts
···22// See the LICENCE file in the repository root for full licence text.
3344export function toggleCart(flag: boolean) {
55- $('.js-store-add-to-cart').prop('disabled', !flag);
66- $('#product-form').data('disabled', !flag);
55+ const body = window.newBody;
66+ if (body == null) return;
77+88+ const button = body.querySelector<HTMLButtonElement>('.js-store-add-to-cart');
99+ if (button != null) {
1010+ button.disabled = !flag;
1111+ }
1212+1313+ const form = body.querySelector<HTMLFormElement>('#product-form');
1414+ if (form != null) {
1515+ $(form).data('disabled', !flag);
1616+ }
717}
···1010 'missing' => 'Požadovaná stránka ":keyword" nebyla nalezena.',
1111 'missing_title' => 'Nenalezeno',
1212 'missing_translation' => 'Požadovaná stránka nebyla nalezena pro zvolený jazyk.',
1313- 'needs_cleanup_or_rewrite' => 'Tato stránka nesplňuje standardy osu! wiki a potřebuje být vylepšena nebo přepsáno. Pokud chcete, můžete pomoci s aktualizací článku!',
1313+ 'needs_cleanup_or_rewrite' => 'Tato stránka nesplňuje standardy osu! wiki a potřebuje být vylepšena nebo přepsána. Pokud chcete, můžete pomoci s aktualizací článku!',
1414 'search' => 'Prohledat existující stránky pro :link.',
1515 'stub' => 'Tento článek je neúplný a čeká na někoho, kdo ho rozšíří.',
1616 'toc' => 'Obsah',
+1-1
resources/lang/de/authorization.php
···8181 ],
82828383 'contest' => [
8484- 'judging_not_active' => '',
8484+ 'judging_not_active' => 'Bewertung für diesen Wettbewerb ist nicht aktiv.',
8585 'voting_over' => 'Stimmen können nach dem Abstimmungsende nicht mehr geändert werden.',
86868787 'entry' => [
+10-10
resources/lang/de/contest.php
···1414 ],
15151616 'judge' => [
1717- 'hide_judged' => '',
1818- 'nav_title' => '',
1919- 'no_current_vote' => '',
1717+ 'hide_judged' => 'bewertete Einträge ausblenden',
1818+ 'nav_title' => 'Bewerten',
1919+ 'no_current_vote' => 'Du hast noch nicht abgestimmt.',
2020 'update' => 'aktualisieren',
2121 'validation' => [
2222- 'missing_score' => '',
2323- 'contest_vote_judged' => '',
2222+ 'missing_score' => 'fehlende Punktzahl',
2323+ 'contest_vote_judged' => 'Abstimmen bei bewerteten Wettbewerben nicht möglich',
2424 ],
2525- 'voted' => '',
2525+ 'voted' => 'Du hast für diesen Eintrag bereits abgestimmt.',
2626 ],
27272828 'judge_results' => [
2929- '_' => '',
3030- 'creator' => '',
2929+ '_' => 'Jury-Ergebnisse',
3030+ 'creator' => 'Ersteller',
3131 'score' => 'Ergebnis',
3232 'total_score' => 'Gesamtergebnis',
3333 ],
34343535 'voting' => [
3636- 'judge_link' => '',
3737- 'judged_notice' => '',
3636+ 'judge_link' => 'Du bist ein Juror bei diesem Wettbewerb. Bewerte die Beiträge hier!',
3737+ 'judged_notice' => 'Dieser Wettbewerb läuft über das Bewertungssystem, die Jury bearbeitet derzeit die Beiträge.',
3838 'login_required' => 'Bitte einloggen, um abzustimmen',
3939 'over' => 'Die Abstimmung für diesen Wettbewerb ist beendet',
4040 'show_voted_only' => 'Stimmen anzeigen',
···2828 'non_passing' => 'Nur erfolgreiche Scores geben pp',
2929 'no_pp' => 'Für diesen Score werden keine pp vergeben',
3030 'processing' => 'Dieser Score wird noch berechnet und in Kürze angezeigt',
3131- 'no_rank' => '',
3131+ 'no_rank' => 'Diese Punktzahl hat keinen Rang, da sie unranked oder zum Löschen markiert ist',
3232 ],
3333];
···97979898 'force_reactivation' => [
9999 'reason' => [
100100+ 'inactive' => "Your account hasn't been used in a long time.",
100101 'inactive_different_country' => "Your account hasn't been used in a long time.",
101102 ],
102103 ],
···44// See the LICENCE file in the repository root for full licence text.
5566return [
77- 'achievement' => '<strong><em>:user</em></strong> on saanut "<strong>:achievement</strong>" mitalin!',
77+ 'achievement' => '<strong><em>:user</em></strong> on saanut "<strong>:achievement</strong>" -mitalin!',
88 'beatmap_playcount' => 'Karttaa :beatmap on pelattu :count kertaa!',
99 'beatmapset_approve' => ':beatmapset käyttäjältä <strong>:user</strong> on nyt :approval',
1010 'beatmapset_delete' => ':beatmapset on poistettu.',
···106106 'cancel_not_allowed' => 'Tätä tilausta ei voi peruuttaa tällä hetkellä.',
107107 'invoice' => 'Näytä lasku',
108108 'no_orders' => 'Ei tilauksia katsottavissa.',
109109- 'paid_on' => 'Tilaus laitettu :date',
109109+ 'paid_on' => 'Tilaus tehty :date',
110110 'resume' => 'Jatka kassalle',
111111 'shipping_and_handling' => 'Toimitus & käsittely',
112112 'shopify_expired' => 'Tämän tilauksen kassalinkki on vanhentunut.',
···136136137137 'not_modifiable_exception' => [
138138 'cancelled' => 'Et voi muokata tilaustasi, sillä se on peruuntunut.',
139139- 'checkout' => 'Et voi muokata tilaustasi, koska sitä käsitellään vielä.', // checkout and processing should have the same message.
139139+ 'checkout' => 'Et voi muokata tilaustasi silloin kun sitä käsitellään.', // checkout and processing should have the same message.
140140 'default' => 'Tilausta ei voi muokata',
141141 'delivered' => 'Et voi muokata tilaustasi, sillä se on jo toimitettu.',
142142 'paid' => 'Et voi muokata tilaustasi, sillä se on jo maksettu.',
143143- 'processing' => 'Et voi muokata tilaustasi, koska sitä käsitellään vielä.',
143143+ 'processing' => 'Et voi muokata tilaustasi silloin kun sitä käsitellään.',
144144 'shipped' => 'Et voi muokata tilaustasi, sillä se on jo matkalla.',
145145 ],
146146
+11-11
resources/lang/fi/users.php
···259259 'discussions' => [
260260 'title' => 'Keskustelut',
261261 'title_longer' => 'Viimeaikaiset keskustelut',
262262- 'show_more' => 'nää lisää keskusteluja',
262262+ 'show_more' => 'katso lisää keskusteluja',
263263 ],
264264 'events' => [
265265 'title' => 'Tapahtumat',
266266- 'title_longer' => 'Viimeisimmät tapahtumat',
267267- 'show_more' => 'nää lisää tapahtumia',
266266+ 'title_longer' => 'Viimeaikaiset tapahtumat',
267267+ 'show_more' => 'katso lisää tapahtumia',
268268 ],
269269 'historical' => [
270270 'title' => 'Historialliset',
···279279 ],
280280 'recent_plays' => [
281281 'accuracy' => 'tarkkuus: :percentage',
282282- 'title' => 'Viimeisimmät pelaukset (24t)',
282282+ 'title' => 'Viimeaikaiset pelaukset (24t)',
283283 ],
284284 'replays_watched_counts' => [
285285 'title' => 'Uusintojen katsomishistoria',
···348348 ],
349349 'posts' => [
350350 'title' => 'Julkaisut',
351351- 'title_longer' => 'Viimeisimmät julkaisut',
352352- 'show_more' => 'Katso lisää julkaisuja',
351351+ 'title_longer' => 'Viimeaikaiset julkaisut',
352352+ 'show_more' => 'katso lisää julkaisuja',
353353 ],
354354 'recent_activity' => [
355355 'title' => 'Viimeisimmät',
···384384 'given' => 'Annetut äänet (viimeiset 3 kuukautta)',
385385 'received' => 'Saadut äänet (viimeiset 3 kuukautta)',
386386 'title' => 'Äänet',
387387- 'title_longer' => 'Viimeisimmät Äänet',
387387+ 'title_longer' => 'Viimeaikaiset äänet',
388388 'vote_count' => ':count_delimited ääni|:count_delimited ääntä',
389389 ],
390390 'account_standing' => [
···393393 'remaining_silence' => '<strong>:username</strong> pystyy puhumaan seuraavan kerran :duration.',
394394395395 'recent_infringements' => [
396396- 'title' => 'Viimeisimmät rikkomukset',
396396+ 'title' => 'Viimeaikaiset rikkomukset',
397397 'date' => 'päivä',
398398 'action' => 'toiminto',
399399 'length' => 'pituus',
···421421 ],
422422 'not_found' => [
423423 'reason_1' => 'Käyttäjänimi saattaa olla vaihtunut.',
424424- 'reason_2' => 'Käyttäjä voi olla tilapaisesti poissa käytöstä tietoturvasyistä tai väärinkäytön seurauksena.',
424424+ 'reason_2' => 'Käyttäjätunnus voi olla tilapäisesti pois käytöstä tietoturvasyistä tai väärinkäytön seurauksena.',
425425 'reason_3' => 'Teit mahdollisesti kirjoitusvirheen!',
426426- 'reason_header' => 'Tähän on lukuisia mahdollisia syitä:',
426426+ 'reason_header' => 'Tähän on muutama mahdollinen syy:',
427427 'title' => 'Käyttäjää ei löytynyt! ;_;',
428428 ],
429429 'page' => [
430430- 'button' => 'Muokkaa profiilisivua',
430430+ 'button' => 'muokkaa profiilisivua',
431431 'description' => '<strong>minä!</strong> on henkilökohtainen alue profiilisivullasi, jota voit muokata.',
432432 'edit_big' => 'Muokkaa minua!',
433433 'placeholder' => 'Kirjoita sivun sisältö tähän',
···92929393 'forum' => [
9494 'moderate' => [
9595- 'no_permission' => 'Kamu tdak memiliki izin untuk memoderasi forum ini.',
9595+ 'no_permission' => 'Kamu tidak memiliki izin untuk memoderasi forum ini.',
9696 ],
97979898 'post' => [
+2-2
resources/lang/it/artist.php
···13131414 'beatmaps' => [
1515 '_' => 'Beatmap',
1616- 'download' => 'Scarica il Template della Beatmap',
1717- 'download-na' => 'Template della Beatmap non ancora disponibile',
1616+ 'download' => 'scarica il template della beatmap',
1717+ 'download-na' => 'template della beatmap non ancora disponibile',
1818 ],
19192020 'index' => [
+2-2
resources/lang/it/authorization.php
···99 'require_verification' => 'Esegui la verifica per poter continuare.',
1010 'restricted' => "Non puoi farlo mentre sei limitato.",
1111 'silenced' => "Non puoi farlo mentre sei silenziato.",
1212- 'unauthorized' => 'Accesso Negato.',
1212+ 'unauthorized' => 'Accesso negato.',
13131414 'beatmap_discussion' => [
1515 'destroy' => [
···3434 'bot' => "Non puoi votare in una discussione creata da un bot",
3535 'limit_exceeded' => 'Attendi un po\' prima di aggiungere più voti',
3636 'owner' => "Non puoi votare la tua discussione.",
3737- 'wrong_beatmapset_state' => 'Puoi votare solo su discussioni di beatmap in attesa.',
3737+ 'wrong_beatmapset_state' => 'Puoi votare solo sulle discussioni di beatmap in attesa.',
3838 ],
3939 ],
4040
···8181 ],
82828383 'contest' => [
8484- 'judging_not_active' => '',
8484+ 'judging_not_active' => 'Beoordeling is niet actief voor deze wedstrijd.',
8585 'voting_over' => 'Je kan je stem niet meer veranderen nadat de stemperiode van deze wedstrijd is afgelopen.',
86868787 'entry' => [
+2-2
resources/lang/nl/beatmaps.php
···213213214214 'rank_estimate' => [
215215 '_' => 'Deze map staat gepland om ranked te worden op :date als er geen problemen worden gevonden. Het is #:position in de :queue.',
216216- 'unresolved_problems' => '',
217217- 'problems' => '',
216216+ 'unresolved_problems' => 'Deze map is momenteel geblokkeerd om de Gekwalificeerde sectie te verlaten totdat :problems zijn opgelost.',
217217+ 'problems' => 'deze problemen',
218218 'on' => 'op :date',
219219 'queue' => 'ranking wachtlijst',
220220 'soon' => 'binnenkort',
+13-13
resources/lang/nl/contest.php
···1414 ],
15151616 'judge' => [
1717- 'hide_judged' => '',
1818- 'nav_title' => '',
1919- 'no_current_vote' => '',
2020- 'update' => '',
1717+ 'hide_judged' => 'verberg beoordeelde items',
1818+ 'nav_title' => 'beoordeel',
1919+ 'no_current_vote' => 'je hebt nog niet gestemd.',
2020+ 'update' => 'werk bij',
2121 'validation' => [
2222- 'missing_score' => '',
2323- 'contest_vote_judged' => '',
2222+ 'missing_score' => 'ontbrekende score',
2323+ 'contest_vote_judged' => 'kan niet stemmen in beoordeelde wedstrijden',
2424 ],
2525- 'voted' => '',
2525+ 'voted' => 'Je hebt al een stem ingediend voor dit item.',
2626 ],
27272828 'judge_results' => [
2929- '_' => '',
3030- 'creator' => '',
3131- 'score' => '',
3232- 'total_score' => '',
2929+ '_' => 'Beoordelingsresultaten',
3030+ 'creator' => 'maker',
3131+ 'score' => 'Score',
3232+ 'total_score' => 'totale score',
3333 ],
34343535 'voting' => [
3636- 'judge_link' => '',
3737- 'judged_notice' => '',
3636+ 'judge_link' => 'Jij bent een jurylid voor deze wedstrijd. Beoordeel de inzendingen hier!',
3737+ 'judged_notice' => 'Deze wedstrijd maakt gebruik van het jurysysteem, de jury verwerkt momenteel de inzendingen.',
3838 'login_required' => 'Log in om te kunnen stemmen.',
3939 'over' => 'Je kan niet meer stemmen in deze wedstrijd',
4040 'show_voted_only' => 'Toon gestemde stemmen',
···3737 'username' => 'Vul e-mail adres of gebruikersnaam in',
38383939 'reason' => [
4040- 'inactive_different_country' => "",
4040+ 'inactive_different_country' => "Uw account is een lange tijd niet gebruikt. Om uw accountbeveiliging te verzekeren, reset uw wachtwoord.",
4141 ],
4242 'support' => [
4343 '_' => 'Meer hulp nodig? Neem contact met ons op via onze :button.',
···2626 'status' => [
2727 'non_best' => 'Enkel je beste score op een beatmap levert pp op',
2828 'non_passing' => 'Alleen geslaagde scores leveren pp op',
2929- 'no_pp' => '',
2929+ 'no_pp' => 'pp word niet opgeleverd voor deze score',
3030 'processing' => 'Deze score wordt nog berekend en zal zo dadelijk getoond worden',
3131- 'no_rank' => '',
3131+ 'no_rank' => 'Deze score heeft geen rang omdat deze ongerangschikt is of gemarkeerd is voor verwijdering',
3232 ],
3333];
+5-5
resources/lang/nl/store.php
···7878 ],
7979 'prepared' => [
8080 'title' => 'Je bestelling wordt voorbereid!',
8181- 'line_1' => '',
8282- 'line_2' => '',
8181+ 'line_1' => 'Wacht alsjeblieft iets langer voor de verzending. Tracking-informatie zal hier verschijnen zodra de bestelling is verwerkt en verzonden. Dit kan tot 5 dagen duren (maar vaak minder!) afhankelijk van hoe druk we zijn.',
8282+ 'line_2' => 'We verzenden alle bestellingen vanuit Japan d.m.v. een aantal bezorgdiensten afhankelijk van het gewicht en de waarde. Dit gebied zal worden bijgewerkt met details zodra we de bestelling hebben verzonden.',
8383 ],
8484 'processing' => [
8585 'title' => 'Uw betaling is nog niet bevestigd!',
···9191 ],
9292 'shipped' => [
9393 'title' => 'Je bestelling is verzonden!',
9494- 'tracking_details' => '',
9494+ 'tracking_details' => 'Tracking-details volgen:',
9595 'no_tracking_details' => [
9696- '_' => "",
9696+ '_' => "We hebben geen tracking-details omdat we jouw pakket via Air Mail verzonden hebben, maar je kunt deze verwachten binnen 1-3 weken. In Europa kan de douane soms vertraging buiten onze controle veroorzaken. Als je vragen hebt, antwoord op de bestelbevestigings-e-mail die je hebt ontvangen (of :link).",
9797 'link_text' => 'stuur ons een email',
9898 ],
9999 ],
···157157 'thanks' => [
158158 'title' => 'Bedankt voor je bestelling!',
159159 'line_1' => [
160160- '_' => '',
160160+ '_' => 'Je zal binnenkort een bevestigings-e-mail ontvangen. Als je vragen hebt, :link!',
161161 'link_text' => 'contacteer ons',
162162 ],
163163 ],
···1919 'new_confirmation' => 'potvrdenie emailu',
2020 'title' => 'Email',
2121 'locked' => [
2222- '_' => '',
2323- 'accounts' => '',
2222+ '_' => 'Prosím kontaktuje :accounts ak potrebujete aktualizovať svoj email.',
2323+ 'accounts' => 'tím podpory účtu',
2424 ],
2525 ],
26262727 'legacy_api' => [
2828- 'api' => '',
2929- 'irc' => '',
2828+ 'api' => 'api',
2929+ 'irc' => 'irc',
3030 'title' => '',
3131 ],
3232···3838 ],
39394040 'profile' => [
4141- 'country' => '',
4141+ 'country' => 'krajina',
4242 'title' => 'Profil',
43434444 'country_change' => [
4545- '_' => "",
4646- 'update_link' => '',
4545+ '_' => "Krajina vášho účtu nezodpovedá krajine vášho bydliska. :update_link.",
4646+ 'update_link' => 'Aktualizovať na :country',
4747 ],
48484949 'user' => [
···6363 ],
64646565 'github_user' => [
6666- 'info' => "",
6767- 'link' => '',
6868- 'title' => '',
6969- 'unlink' => '',
6666+ 'info' => "Ak ste prispievateľom do úložisiek s otvoreným zdrojovým kódom osu!, prepojenie účtu GitHub tu priradí vaše záznamy z denníku zmien k vášmu osu! profilu. Účty GitHub bez histórie príspevkov do osu! nemožno prepojiť.",
6767+ 'link' => 'Prepoj GitHub účet',
6868+ 'title' => 'GitHub',
6969+ 'unlink' => 'Preruš prepojenie GitHub účtu',
70707171 'error' => [
7272- 'already_linked' => '',
7373- 'no_contribution' => '',
7474- 'unverified_email' => '',
7272+ 'already_linked' => 'Tento GitHub účet je už prepojený na iného užívateľa.',
7373+ 'no_contribution' => 'GitHub účet bez histórie príspevkov do úložisiek osu! nemožno prepojiť.',
7474+ 'unverified_email' => 'Prosím skontrolujte svoj primárny email v GitHub, potom skúste znova prepojiť svoj účet.',
7575 ],
7676 ],
7777
+3-2
resources/lang/sk/api.php
···1717 'identify' => 'Identifikovať vás a prezerať váš verejný profil.',
18181919 'chat' => [
2020- 'read' => '',
2020+ 'read' => 'Prečítajte si správy vo vašom mene.
2121+',
2122 'write' => 'Posielajte správy vo vašom mene.',
2222- 'write_manage' => '',
2323+ 'write_manage' => 'Pripojte sa a opustite kanály vo vašom mene.',
2324 ],
24252526 'forum' => [
+2-2
resources/lang/sk/authorization.php
···8181 ],
82828383 'contest' => [
8484- 'judging_not_active' => '',
8484+ 'judging_not_active' => 'Hodnotenie tejto súťaže nie je aktívne.',
8585 'voting_over' => 'Po tom, čo sa hlasovacie obdobie pre túto súťaž ukončilo, svoj hlas nemôžete zmeniť.',
86868787 'entry' => [
···172172173173 'score' => [
174174 'pin' => [
175175- 'disabled_type' => "",
175175+ 'disabled_type' => "Tento typ skóre sa nedá pripnúť",
176176 'failed' => "",
177177 'not_owner' => 'Skóre môže pripnúť iba pôvodný hráč.',
178178 'too_many' => 'Už bolo pripnuté maximum skóre.',
···33 See the LICENCE file in the repository root for full licence text.
44--}}
55@php
66- $customization = auth()->user()->profileCustomization();
66+ $customization = Auth::user()->profileCustomization();
77@endphp
88<div class="account-edit">
99 <div class="account-edit__section">