Skip to content

Add user cover preset model #11081

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions app/Http/Controllers/UserCoverPresetsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

namespace App\Http\Controllers;

use App\Models\UserCoverPreset;
use Symfony\Component\HttpFoundation\Response;

class UserCoverPresetsController extends Controller
{
public function __construct()
{
$this->middleware('auth');

parent::__construct();
}

public function batchActivate(): Response
{
$params = get_params(\Request::all(), null, [
'ids:int[]',
'active:bool',
]);
if (!isset($params['active'])) {
abort(422, 'parameter "active" is missing');
}
UserCoverPreset::whereKey($params['ids'] ?? [])->update(['active' => $params['active']]);

return response(null, 204);
}

public function index(): Response
{
priv_check('UserCoverPresetManage')->ensureCan();

return ext_view('user_cover_presets.index', [
'items' => UserCoverPreset::orderBy('id', 'ASC')->get(),
]);
}

public function store(): Response
{
priv_check('UserCoverPresetManage')->ensureCan();

try {
$files = \Request::file('files') ?? [];
foreach ($files as $file) {
$item = \DB::transaction(function () use ($file) {
$item = UserCoverPreset::create();
$item->file()->store($file->getRealPath());
$item->saveOrExplode();

return $item;
});
$hash ??= "#cover-{$item->getKey()}";
}
\Session::flash('popup', osu_trans('user_cover_presets.store.ok'));
} catch (\Throwable $e) {
\Session::flash('popup', osu_trans('user_cover_presets.store.failed', ['error' => $e->getMessage()]));
}

return ujs_redirect(route('user-cover-presets.index').($hash ?? ''));
}

public function update(string $id): Response
{
priv_check('UserCoverPresetManage')->ensureCan();

$item = UserCoverPreset::findOrFail($id);
$params = get_params(\Request::all(), null, [
'file:file',
'active:bool',
], ['null_missing' => true]);

if ($params['file'] !== null) {
$item->file()->store($params['file']);
$item->save();
}
if ($params['active'] !== null) {
$item->update(['active' => $params['active']]);
}

return ujs_redirect(route('user-cover-presets.index').'#cover-'.$item->getKey());
}
}
31 changes: 31 additions & 0 deletions app/Models/UserCoverPreset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

namespace App\Models;

use App\Libraries\Uploader;
use App\Libraries\User\Cover;

/**
* @property bool $active
* @property \Carbon\Carbon|null $created_at
* @property string|null $filename
* @property int $id
* @property \Carbon\Carbon|null $updated_at
*/
class UserCoverPreset extends Model
{
private Uploader $file;

public function file(): Uploader
{
return $this->file ??= new Uploader(
'user-cover-presets',
$this,
'filename',
['image' => ['maxDimensions' => Cover::CUSTOM_COVER_MAX_DIMENSIONS]],
);
}
}
5 changes: 5 additions & 0 deletions app/Singletons/OsuAuthorize.php
Original file line number Diff line number Diff line change
Expand Up @@ -2087,6 +2087,11 @@ public function checkMatchView(?User $user, LegacyMatch $match): string
return 'unauthorized';
}

public function checkUserCoverPresetManage(?User $user): ?string
{
return null;
}

/**
* @param User|null $user
* @return string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::create('user_cover_presets', function (Blueprint $table) {
$table->mediumIncrements('id');
$table->string('filename')->nullable(true);
$table->boolean('active')->default(false)->nullable(false);
$table->timestamps();
$table->index('active');
});
}

public function down(): void
{
Schema::dropIfExists('user_cover_presets');
}
};
2 changes: 2 additions & 0 deletions resources/css/bem-index.less
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,8 @@
@import "bem/user-card";
@import "bem/user-card-brick";
@import "bem/user-cards";
@import "bem/user-cover-preset-replace";
@import "bem/user-cover-preset-table";
@import "bem/user-group-badge";
@import "bem/user-home";
@import "bem/user-home-beatmapset";
Expand Down
5 changes: 5 additions & 0 deletions resources/css/bem/btn-osu-big.less
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,11 @@
border-radius: 100000px;
}

&--rounded-small {
border-radius: 100000px;
padding: 4px 10px;
}

&--rounded-thin {
padding: 8px 15px;
border-radius: 100000px;
Expand Down
3 changes: 3 additions & 0 deletions resources/css/bem/osu-switch-v2.less
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,12 @@
}

.@{_top}__input[type=checkbox][data-indeterminate='true'] + & {
color: hsl(var(--hsl-h1));

&::after {
.fas();
content: '\f068';
opacity: 1;
}
}

Expand Down
13 changes: 13 additions & 0 deletions resources/css/bem/user-cover-preset-replace.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

.user-cover-preset-replace {
display: grid;
gap: 10px;
padding-top: 10px;
border-top: 1px solid hsl(var(--hsl-b1));

&__input {
width: 100%;
}
}
38 changes: 38 additions & 0 deletions resources/css/bem/user-cover-preset-table.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

.user-cover-preset-table {
display: grid;
grid-template-columns: auto 150px 1fr;

&__image {
width: 100%;
}

&__toolbar {
grid-column-start: 2;
grid-column-end: 4;
}

&__row {
padding: 10px;
gap: 10px;

display: grid;
grid-column: ~"1 / 4";
grid-template-columns: subgrid;

&--item {
background: var(--row-bg);
--row-bg: hsl(var(--hsl-b4));

&:hover {
background: hsl(var(--hsl-b2));
}

&:nth-child(even) {
--row-bg: hsl(var(--hsl-b3));
}
}
}
}
6 changes: 6 additions & 0 deletions resources/js/entrypoints/user-cover-presets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

import UserCoverPresetBatchActivate from 'user-cover-preset-batch-activate';

new UserCoverPresetBatchActivate();
132 changes: 132 additions & 0 deletions resources/js/user-cover-preset-batch-activate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

import { route } from 'laroute';
import { trans, transChoice } from 'utils/lang';
import { popup } from 'utils/popup';
import { reloadPage } from 'utils/turbolinks';

const checkboxSelector = '.js-user-cover-preset-batch-enable--checkbox';

export default class UserCoverPresetBatchActivate {
private lastSelected: HTMLInputElement | null = null;
private xhr: JQuery.jqXHR<void> | null = null;

constructor() {
$(document)
.on('click', '.js-user-cover-preset-batch-enable', this.handleEvent)
.on('turbolinks:before-cache', this.cleanup);
}

private applySelected(active: boolean) {
const ids = [...this.checkboxes()]
.filter((el) => el.checked)
.map((el) => el.dataset.id);
const count = ids.length;

if (count === 0) {
popup('no covers selected');
return;
}

if (!confirm(trans('user_cover_presets.index.batch_confirm._', {
action: trans(`user_cover_presets.index.batch_confirm.${active ? 'enable' : 'disable'}`),
items: transChoice('user_cover_presets.index.batch_confirm.items', count),
}))) {
return;
}

this.xhr = $.post(route('user-cover-presets.batch-activate'), { active, ids });
this.xhr
.done(() => {
reloadPage();
})
.fail((xhr, status) => {
if (status !== 'abort') {
popup('Update failed', 'danger');
}
});
}

private checkboxes() {
return document.querySelectorAll<HTMLInputElement>(checkboxSelector);
}

private readonly cleanup = () => {
this.lastSelected = null;
this.xhr?.abort();
this.xhr = null;
};

private readonly handleEvent = (e: JQuery.ClickEvent<Document, unknown, HTMLElement, HTMLElement>) => {
const target = e.currentTarget;

switch (target.dataset.action) {
case 'disable-selected': return this.applySelected(false);
case 'enable-selected': return this.applySelected(true);
case 'select': return this.select(target, e);
case 'select-all': return this.toggleAll(target as HTMLInputElement);
}
};

private readonly select = (target: HTMLElement, e: JQuery.ClickEvent) => {
const checkbox = target.querySelector(checkboxSelector);
if (checkbox instanceof HTMLInputElement) {
if ((e.originalEvent?.shiftKey ?? false) && this.lastSelected != null) {
const checked = this.lastSelected.checked;
let started = false;
for (const el of this.checkboxes()) {
if (el === checkbox) {
el.checked = checked;
}
if (el === this.lastSelected || el === checkbox) {
if (started) {
break;
} else {
started = true;
}
continue;
}
if (started) {
el.checked = checked;
}
}
}
this.lastSelected = checkbox;
}
this.syncToggleState();
};

private selectAllCheckbox() {
const ret = document.querySelector('.js-user-cover-preset-batch-enable--select-all');
if (!(ret instanceof HTMLInputElement)) {
throw new Error('select all checkbox element is not HTMLInputElement');
}

return ret;
}

private syncToggleState() {
const selectAllCheckbox = this.selectAllCheckbox();
let state: boolean | null = null;
for (const el of this.checkboxes()) {
if (state == null) {
selectAllCheckbox.checked = state = el.checked;
selectAllCheckbox.dataset.indeterminate = 'false';
} else {
if (state !== el.checked) {
selectAllCheckbox.dataset.indeterminate = 'true';
break;
}
}
}
}

private readonly toggleAll = (target: HTMLInputElement) => {
const checked = target.checked;
for (const el of this.checkboxes()) {
el.checked = checked;
}
target.dataset.indeterminate = 'false';
};
}
3 changes: 3 additions & 0 deletions resources/lang/en/page_title.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@
'tournaments_controller' => [
'_' => 'tournaments',
],
'user_cover_presets_controller' => [
'_' => 'user cover presets',
],
'users_controller' => [
'_' => 'player info',
'create' => 'create account',
Expand Down
Loading