Script pane, realtime peer sync, and logout button
- Script pane: PDF tab beside annotations; "Add script annotation" arms highlight mode on the draft, ✓/✕ to confirm/cancel, auto-scrolls to the active annotation's highlights during playback. Adds Project.script. - Realtime: Django Channels websocket per project broadcasts attributed scene deltas after each save; peers merge them in (keeping local drafts). - Logout link in the editor project-bar. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
595ac5ba72
commit
d3c8dcbdf2
13 changed files with 307 additions and 11 deletions
22
scenes/consumers.py
Normal file
22
scenes/consumers.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import json
|
||||
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
|
||||
|
||||
class SceneConsumer(AsyncWebsocketConsumer):
|
||||
"""One connection per open project. Joins the project's group and relays
|
||||
scene deltas the server broadcasts (see scenes.views.scene). Read-only:
|
||||
edits still go through the PUT endpoint, which is the broadcast source."""
|
||||
|
||||
async def connect(self):
|
||||
self.pk = self.scope['url_route']['kwargs']['pk']
|
||||
self.group = f'project_{self.pk}'
|
||||
await self.channel_layer.group_add(self.group, self.channel_name)
|
||||
await self.accept()
|
||||
|
||||
async def disconnect(self, code):
|
||||
await self.channel_layer.group_discard(self.group, self.channel_name)
|
||||
|
||||
# broadcast handler: {type: "scene.delta", delta: {...}}
|
||||
async def scene_delta(self, event):
|
||||
await self.send(text_data=json.dumps(event['delta']))
|
||||
18
scenes/migrations/0006_project_script.py
Normal file
18
scenes/migrations/0006_project_script.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 6.0.6 on 2026-06-29 08:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('scenes', '0005_project_thumbnail_error_project_thumbnail_manifest_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='script',
|
||||
field=models.FileField(blank=True, null=True, upload_to='scripts/'),
|
||||
),
|
||||
]
|
||||
|
|
@ -16,6 +16,7 @@ class Project(models.Model):
|
|||
name = models.CharField(max_length=200)
|
||||
otio = models.FileField(upload_to="otio/", blank=True, null=True)
|
||||
clip = models.FileField(upload_to="clips/", blank=True, null=True)
|
||||
script = models.FileField(upload_to="scripts/", blank=True, null=True)
|
||||
fps = models.FloatField(default=NTSC_24)
|
||||
thumbnail_status = models.CharField(max_length=20, default="missing")
|
||||
thumbnail_manifest = models.JSONField(default=dict, blank=True)
|
||||
|
|
|
|||
7
scenes/routing.py
Normal file
7
scenes/routing.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from django.urls import path
|
||||
|
||||
from .consumers import SceneConsumer
|
||||
|
||||
websocket_urlpatterns = [
|
||||
path('ws/projects/<int:pk>/', SceneConsumer.as_asgi()),
|
||||
]
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import json
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from django.contrib.auth import authenticate, login as dj_login, logout as dj_logout
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
|
|
@ -53,6 +55,7 @@ def _meta(request, project):
|
|||
"owner": project.owner.get_username(),
|
||||
"otio": request.build_absolute_uri(project.otio.url) if project.otio else None,
|
||||
"clip": request.build_absolute_uri(project.clip.url) if project.clip else None,
|
||||
"script": request.build_absolute_uri(project.script.url) if project.script else None,
|
||||
"thumbnail_status": project.thumbnail_status,
|
||||
"thumbnail_error": project.thumbnail_error,
|
||||
"thumbnails": thumbnails or None,
|
||||
|
|
@ -150,6 +153,15 @@ def scene(request, pk):
|
|||
if any(counts.values()): # don't log a no-op save
|
||||
Revision.objects.create(project=p, user=request.user, summary=summary,
|
||||
annotations=annotation_layer(p.scene))
|
||||
# push the attributed delta to peers viewing this project
|
||||
scene_groups = (p.scene or {}).get("groups", {})
|
||||
async_to_sync(get_channel_layer().group_send)(
|
||||
f"project_{p.pk}",
|
||||
{"type": "scene.delta",
|
||||
"delta": {"changed": {gid: scene_groups[gid]
|
||||
for gid in changed if gid in scene_groups},
|
||||
"deleted": list(deleted),
|
||||
"by": request.user.get_username()}})
|
||||
if "fps" in data:
|
||||
p.fps = data["fps"]
|
||||
p.save(update_fields=["scene", "fps", "updated"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue