Merge mobile-view-match-highlights: frame-accurate scrubbing, mobile UX, collapsed action menu

This commit is contained in:
ghassan 2026-08-09 02:27:48 +03:00
commit ed921a4ccf
7 changed files with 690 additions and 162 deletions

View File

@ -18,7 +18,7 @@ class MatchEventController extends Controller
$request->validate([ $request->validate([
'round_number' => 'required|integer|min:1', 'round_number' => 'required|integer|min:1',
'name' => 'nullable|string|max:50', 'name' => 'nullable|string|max:50',
'start_time_seconds' => 'nullable|integer|min:0', 'start_time_seconds' => 'nullable|numeric|min:0',
]); ]);
// Check if user owns the video // Check if user owns the video
@ -45,7 +45,7 @@ class MatchEventController extends Controller
$request->validate([ $request->validate([
'round_number' => 'sometimes|integer|min:1', 'round_number' => 'sometimes|integer|min:1',
'name' => 'required|string|max:50', 'name' => 'required|string|max:50',
'start_time_seconds' => 'nullable|integer|min:0', 'start_time_seconds' => 'nullable|numeric|min:0',
]); ]);
// Check if user owns the video // Check if user owns the video
@ -87,7 +87,7 @@ class MatchEventController extends Controller
{ {
$request->validate([ $request->validate([
'round_id' => 'required|exists:match_rounds,id', 'round_id' => 'required|exists:match_rounds,id',
'timestamp_seconds' => 'required|integer|min:0', 'timestamp_seconds' => 'required|numeric|min:0',
'action' => 'required|string|max:255', 'action' => 'required|string|max:255',
'points' => 'required|integer|min:1', 'points' => 'required|integer|min:1',
'competitor' => 'required|in:blue,red', 'competitor' => 'required|in:blue,red',
@ -150,7 +150,7 @@ class MatchEventController extends Controller
public function updatePoint(Request $request, MatchPoint $point) public function updatePoint(Request $request, MatchPoint $point)
{ {
$request->validate([ $request->validate([
'timestamp_seconds' => 'required|integer|min:0', 'timestamp_seconds' => 'required|numeric|min:0',
'action' => 'required|string|max:255', 'action' => 'required|string|max:255',
'points' => 'required|integer|min:1', 'points' => 'required|integer|min:1',
'competitor' => 'required|in:blue,red', 'competitor' => 'required|in:blue,red',
@ -202,8 +202,8 @@ class MatchEventController extends Controller
public function storeReview(Request $request, Video $video) public function storeReview(Request $request, Video $video)
{ {
$request->validate([ $request->validate([
'start_time_seconds' => 'required|integer|min:0', 'start_time_seconds' => 'required|numeric|min:0',
'end_time_seconds' => 'nullable|integer|min:0', 'end_time_seconds' => 'nullable|numeric|min:0',
'note' => 'required|string|max:1000', 'note' => 'required|string|max:1000',
'coach_name' => 'required|string|max:100', 'coach_name' => 'required|string|max:100',
'emoji' => 'nullable|string|max:10', 'emoji' => 'nullable|string|max:10',
@ -238,8 +238,8 @@ class MatchEventController extends Controller
public function updateReview(Request $request, CoachReview $review) public function updateReview(Request $request, CoachReview $review)
{ {
$request->validate([ $request->validate([
'start_time_seconds' => 'required|integer|min:0', 'start_time_seconds' => 'required|numeric|min:0',
'end_time_seconds' => 'nullable|integer|min:0', 'end_time_seconds' => 'nullable|numeric|min:0',
'note' => 'required|string|max:1000', 'note' => 'required|string|max:1000',
'coach_name' => 'required|string|max:100', 'coach_name' => 'required|string|max:100',
'emoji' => 'nullable|string|max:10', 'emoji' => 'nullable|string|max:10',

View File

@ -23,8 +23,10 @@ class CoachReview extends Model
]; ];
protected $casts = [ protected $casts = [
'position_x' => 'float', 'position_x' => 'float',
'position_y' => 'float', 'position_y' => 'float',
'start_time_seconds' => 'float',
'end_time_seconds' => 'float',
]; ];
public function video(): BelongsTo public function video(): BelongsTo

View File

@ -22,6 +22,10 @@ class MatchPoint extends Model
'score_red', 'score_red',
]; ];
protected $casts = [
'timestamp_seconds' => 'float',
];
public function video(): BelongsTo public function video(): BelongsTo
{ {
return $this->belongsTo(Video::class); return $this->belongsTo(Video::class);

View File

@ -19,7 +19,7 @@ class MatchRound extends Model
]; ];
protected $casts = [ protected $casts = [
'start_time_seconds' => 'integer', 'start_time_seconds' => 'float',
]; ];
public function video(): BelongsTo public function video(): BelongsTo

View File

@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Frame-accurate timestamps: the match highlights scrubber lands on
* frame boundaries (1/fps seconds). Storing these as INTEGER seconds
* throws away sub-second precision, so we promote the columns to
* decimal(10,3) 3 decimals cover up to 1000 fps and centuries of
* runtime, and existing integer values migrate losslessly.
*/
public function up(): void
{
// MatchRound.start_time_seconds is used to jump to a round's opening
// frame too, so bring it along.
Schema::table('match_rounds', function (Blueprint $table) {
$table->decimal('start_time_seconds', 10, 3)->nullable()->change();
});
Schema::table('match_points', function (Blueprint $table) {
$table->decimal('timestamp_seconds', 10, 3)->change();
});
Schema::table('coach_reviews', function (Blueprint $table) {
$table->decimal('start_time_seconds', 10, 3)->change();
$table->decimal('end_time_seconds', 10, 3)->nullable()->change();
});
}
public function down(): void
{
// Round back to integer on rollback — sub-second data would be
// truncated but nothing catastrophic happens.
Schema::table('match_rounds', function (Blueprint $table) {
$table->integer('start_time_seconds')->nullable()->change();
});
Schema::table('match_points', function (Blueprint $table) {
$table->integer('timestamp_seconds')->change();
});
Schema::table('coach_reviews', function (Blueprint $table) {
$table->integer('start_time_seconds')->change();
$table->integer('end_time_seconds')->nullable()->change();
});
}
};

View File

@ -71,10 +71,14 @@
color: var(--brand-red) !important; color: var(--brand-red) !important;
} }
/* Single Action dropdown for every viewport the individual
.desktop-action buttons are collapsed into this one menu. */
.mobile-action-dropdown { .mobile-action-dropdown {
display: none; display: inline-block;
position: relative; position: relative;
margin-left: auto;
} }
.video-actions > .desktop-action { display: none !important; }
.mobile-action-dropdown .dropdown-menu { .mobile-action-dropdown .dropdown-menu {
right: 0; right: 0;

File diff suppressed because it is too large Load Diff