mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-12-07 20:52:30 -05:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df98b65704 | ||
|
|
a607996bfa | ||
|
|
79f2e2bb11 | ||
|
|
d54732040a | ||
|
|
e8520bc7cd | ||
|
|
b6e46e7b62 | ||
|
|
36eab954bd | ||
|
|
610716533b | ||
|
|
4f1c4a2b4c |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -25,3 +25,7 @@ yt.readme.md
|
|||||||
frontend-tools/.DS_Store
|
frontend-tools/.DS_Store
|
||||||
static/video_editor/videos/sample-video-30s.mp4
|
static/video_editor/videos/sample-video-30s.mp4
|
||||||
static/video_editor/videos/sample-video-37s.mp4
|
static/video_editor/videos/sample-video-37s.mp4
|
||||||
|
/frontend-tools/video-editor-v2
|
||||||
|
.DS_Store
|
||||||
|
static/video_editor/videos/sample-video-10m.mp4
|
||||||
|
static/video_editor/videos/sample-video-10s.mp4
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -38,7 +38,7 @@ A demo is available at https://demo.mediacms.io
|
|||||||
- **Configurable actions**: allow download, add comments, add likes, dislikes, report media
|
- **Configurable actions**: allow download, add comments, add likes, dislikes, report media
|
||||||
- **Configuration options**: change logos, fonts, styling, add more pages
|
- **Configuration options**: change logos, fonts, styling, add more pages
|
||||||
- **Enhanced video player**: customized video.js player with multiple resolution and playback speed options
|
- **Enhanced video player**: customized video.js player with multiple resolution and playback speed options
|
||||||
- **Multiple transcoding profiles**: sane defaults for multiple dimensions (240p, 360p, 480p, 720p, 1080p) and multiple profiles (h264, h265, vp9)
|
- **Multiple transcoding profiles**: sane defaults for multiple dimensions (144p, 240p, 360p, 480p, 720p, 1080p) and multiple profiles (h264, h265, vp9)
|
||||||
- **Adaptive video streaming**: possible through HLS protocol
|
- **Adaptive video streaming**: possible through HLS protocol
|
||||||
- **Subtitles/CC**: support for multilingual subtitle files
|
- **Subtitles/CC**: support for multilingual subtitle files
|
||||||
- **Scalable transcoding**: transcoding through priorities. Experimental support for remote workers
|
- **Scalable transcoding**: transcoding through priorities. Experimental support for remote workers
|
||||||
@@ -93,20 +93,14 @@ There are two ways to run MediaCMS, through Docker Compose and through installin
|
|||||||
|
|
||||||
A complete guide can be found on the blog post [How to self-host and share your videos in 2021](https://medium.com/@MediaCMS.io/how-to-self-host-and-share-your-videos-in-2021-14067e3b291b).
|
A complete guide can be found on the blog post [How to self-host and share your videos in 2021](https://medium.com/@MediaCMS.io/how-to-self-host-and-share-your-videos-in-2021-14067e3b291b).
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Visit [Configuration](docs/admins_docs.md#5-configuration) page.
|
|
||||||
|
|
||||||
|
|
||||||
## Information for developers
|
|
||||||
Check out the new section on the [Developer Experience](docs/dev_exp.md) page
|
|
||||||
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
* [Users documentation](docs/user_docs.md) page
|
* [Users documentation](docs/user_docs.md) page
|
||||||
* [Administrators documentation](docs/admins_docs.md) page
|
* [Administrators documentation](docs/admins_docs.md) page
|
||||||
* [Developers documentation](docs/developers_docs.md) page
|
* [Developers documentation](docs/developers_docs.md) page
|
||||||
|
* [Configuration](docs/admins_docs.md#5-configuration) page
|
||||||
|
* [Transcoding](docs/transcoding.md) page
|
||||||
|
* [Developer Experience](docs/dev_exp.md) page
|
||||||
|
|
||||||
|
|
||||||
## Technology
|
## Technology
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ CHUNKIZE_VIDEO_DURATION = 60 * 5
|
|||||||
VIDEO_CHUNKS_DURATION = 60 * 4
|
VIDEO_CHUNKS_DURATION = 60 * 4
|
||||||
|
|
||||||
# always get these two, even if upscaling
|
# always get these two, even if upscaling
|
||||||
MINIMUM_RESOLUTIONS_TO_ENCODE = [240, 360]
|
MINIMUM_RESOLUTIONS_TO_ENCODE = [144, 240]
|
||||||
|
|
||||||
# default settings for notifications
|
# default settings for notifications
|
||||||
# not all of them are implemented
|
# not all of them are implemented
|
||||||
@@ -376,16 +376,7 @@ LOGGING = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {"default": {"ENGINE": "django.db.backends.postgresql", "NAME": "mediacms", "HOST": "127.0.0.1", "PORT": "5432", "USER": "mediacms", "PASSWORD": "mediacms", "OPTIONS": {'pool': True}}}
|
||||||
"default": {
|
|
||||||
"ENGINE": "django.db.backends.postgresql",
|
|
||||||
"NAME": "mediacms",
|
|
||||||
"HOST": "127.0.0.1",
|
|
||||||
"PORT": "5432",
|
|
||||||
"USER": "mediacms",
|
|
||||||
"PASSWORD": "mediacms",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
REDIS_LOCATION = "redis://127.0.0.1:6379/1"
|
REDIS_LOCATION = "redis://127.0.0.1:6379/1"
|
||||||
@@ -466,6 +457,7 @@ LANGUAGES = [
|
|||||||
('pt', _('Portuguese')),
|
('pt', _('Portuguese')),
|
||||||
('ru', _('Russian')),
|
('ru', _('Russian')),
|
||||||
('zh-hans', _('Simplified Chinese')),
|
('zh-hans', _('Simplified Chinese')),
|
||||||
|
('sl', _('Slovenian')),
|
||||||
('zh-hant', _('Traditional Chinese')),
|
('zh-hant', _('Traditional Chinese')),
|
||||||
('es', _('Spanish')),
|
('es', _('Spanish')),
|
||||||
('tr', _('Turkish')),
|
('tr', _('Turkish')),
|
||||||
@@ -505,6 +497,10 @@ USE_ROUNDED_CORNERS = True
|
|||||||
ALLOW_VIDEO_TRIMMER = True
|
ALLOW_VIDEO_TRIMMER = True
|
||||||
|
|
||||||
ALLOW_CUSTOM_MEDIA_URLS = False
|
ALLOW_CUSTOM_MEDIA_URLS = False
|
||||||
|
|
||||||
|
# ffmpeg options
|
||||||
|
FFMPEG_DEFAULT_PRESET = "medium" # see https://trac.ffmpeg.org/wiki/Encode/H.264
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# keep a local_settings.py file for local overrides
|
# keep a local_settings.py file for local overrides
|
||||||
from .local_settings import * # noqa
|
from .local_settings import * # noqa
|
||||||
@@ -544,13 +540,5 @@ except ImportError:
|
|||||||
|
|
||||||
|
|
||||||
if GLOBAL_LOGIN_REQUIRED:
|
if GLOBAL_LOGIN_REQUIRED:
|
||||||
# this should go after the AuthenticationMiddleware middleware
|
auth_index = MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware")
|
||||||
MIDDLEWARE.insert(6, "login_required.middleware.LoginRequiredMiddleware")
|
MIDDLEWARE.insert(auth_index + 1, "django.contrib.auth.middleware.LoginRequiredMiddleware")
|
||||||
LOGIN_REQUIRED_IGNORE_PATHS = [
|
|
||||||
r'/accounts/login/$',
|
|
||||||
r'/accounts/logout/$',
|
|
||||||
r'/accounts/signup/$',
|
|
||||||
r'/accounts/password/.*/$',
|
|
||||||
r'/accounts/confirm-email/.*/$',
|
|
||||||
# r'/api/v[0-9]+/',
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
VERSION = "6.1.0"
|
VERSION = "6.3.0"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ DATABASES = {
|
|||||||
"PORT": os.getenv('POSTGRES_PORT', '5432'),
|
"PORT": os.getenv('POSTGRES_PORT', '5432'),
|
||||||
"USER": os.getenv('POSTGRES_USER', 'mediacms'),
|
"USER": os.getenv('POSTGRES_USER', 'mediacms'),
|
||||||
"PASSWORD": os.getenv('POSTGRES_PASSWORD', 'mediacms'),
|
"PASSWORD": os.getenv('POSTGRES_PASSWORD', 'mediacms'),
|
||||||
|
"OPTIONS": {'pool': True},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ services:
|
|||||||
POSTGRES_DB: mediacms
|
POSTGRES_DB: mediacms
|
||||||
TZ: Europe/London
|
TZ: Europe/London
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
|
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -81,6 +81,6 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli","ping"]
|
test: ["CMD", "redis-cli","ping"]
|
||||||
interval: 30s
|
interval: 10s
|
||||||
timeout: 10s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
50
docs/transcoding.md
Normal file
50
docs/transcoding.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Transcoding in MediaCMS
|
||||||
|
|
||||||
|
MediaCMS uses FFmpeg for transcoding media files. Most of the transcoding settings and configurations are defined in `files/helpers.py`.
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
Several transcoding parameters can be customized in `cms/settings.py`:
|
||||||
|
|
||||||
|
### FFmpeg Preset
|
||||||
|
|
||||||
|
The default FFmpeg preset is set to "medium". This setting controls the encoding speed and compression efficiency trade-off.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ffmpeg options
|
||||||
|
FFMPEG_DEFAULT_PRESET = "medium" # see https://trac.ffmpeg.org/wiki/Encode/H.264
|
||||||
|
```
|
||||||
|
|
||||||
|
Available presets include:
|
||||||
|
- ultrafast
|
||||||
|
- superfast
|
||||||
|
- veryfast
|
||||||
|
- faster
|
||||||
|
- fast
|
||||||
|
- medium (default)
|
||||||
|
- slow
|
||||||
|
- slower
|
||||||
|
- veryslow
|
||||||
|
|
||||||
|
Faster presets result in larger file sizes for the same quality, while slower presets provide better compression but take longer to encode.
|
||||||
|
|
||||||
|
### Other Transcoding Settings
|
||||||
|
|
||||||
|
Additional transcoding settings in `settings.py` include:
|
||||||
|
|
||||||
|
- `FFMPEG_COMMAND`: Path to the FFmpeg executable
|
||||||
|
- `FFPROBE_COMMAND`: Path to the FFprobe executable
|
||||||
|
- `DO_NOT_TRANSCODE_VIDEO`: If set to True, only the original video is shown without transcoding
|
||||||
|
- `CHUNKIZE_VIDEO_DURATION`: For videos longer than this duration (in seconds), they get split into chunks and encoded independently
|
||||||
|
- `VIDEO_CHUNKS_DURATION`: Duration of each chunk (must be smaller than CHUNKIZE_VIDEO_DURATION)
|
||||||
|
- `MINIMUM_RESOLUTIONS_TO_ENCODE`: Always encode these resolutions, even if upscaling is required
|
||||||
|
|
||||||
|
## Advanced Configuration
|
||||||
|
|
||||||
|
For more advanced transcoding settings, you may need to modify the following in `files/helpers.py`:
|
||||||
|
|
||||||
|
- Video bitrates for different codecs and resolutions
|
||||||
|
- Audio encoders and bitrates
|
||||||
|
- CRF (Constant Rate Factor) values
|
||||||
|
- Keyframe settings
|
||||||
|
- Encoding parameters for different codecs (H.264, H.265, VP9)
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
from cms.version import VERSION
|
||||||
|
|
||||||
from .frontend_translations import get_translation, get_translation_strings
|
from .frontend_translations import get_translation, get_translation_strings
|
||||||
from .methods import is_mediacms_editor, is_mediacms_manager
|
from .methods import is_mediacms_editor, is_mediacms_manager
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@ def stuff(request):
|
|||||||
ret["USE_SAML"] = settings.USE_SAML
|
ret["USE_SAML"] = settings.USE_SAML
|
||||||
ret["USE_RBAC"] = settings.USE_RBAC
|
ret["USE_RBAC"] = settings.USE_RBAC
|
||||||
ret["USE_ROUNDED_CORNERS"] = settings.USE_ROUNDED_CORNERS
|
ret["USE_ROUNDED_CORNERS"] = settings.USE_ROUNDED_CORNERS
|
||||||
|
ret["VERSION"] = VERSION
|
||||||
|
|
||||||
if request.user.is_superuser:
|
if request.user.is_superuser:
|
||||||
ret["DJANGO_ADMIN_URL"] = settings.DJANGO_ADMIN_URL
|
ret["DJANGO_ADMIN_URL"] = settings.DJANGO_ADMIN_URL
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class IndexRSSFeed(Feed):
|
|||||||
return item.edit_date
|
return item.edit_date
|
||||||
|
|
||||||
def item_link(self, item):
|
def item_link(self, item):
|
||||||
return reverse("get_media") + "?m={0}".format(item.friendly_token)
|
return f"{reverse('get_media')}?m={item.friendly_token}"
|
||||||
|
|
||||||
def item_extra_kwargs(self, item):
|
def item_extra_kwargs(self, item):
|
||||||
item = {
|
item = {
|
||||||
@@ -151,7 +151,7 @@ class SearchRSSFeed(Feed):
|
|||||||
return item.edit_date
|
return item.edit_date
|
||||||
|
|
||||||
def item_link(self, item):
|
def item_link(self, item):
|
||||||
return reverse("get_media") + "?m={0}".format(item.friendly_token)
|
return f"{reverse('get_media')}?m={item.friendly_token}"
|
||||||
|
|
||||||
def item_extra_kwargs(self, item):
|
def item_extra_kwargs(self, item):
|
||||||
item = {
|
item = {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class MediaMetadataForm(forms.ModelForm):
|
|||||||
widgets = {
|
widgets = {
|
||||||
"new_tags": MultipleSelect(),
|
"new_tags": MultipleSelect(),
|
||||||
"description": forms.Textarea(attrs={'rows': 4}),
|
"description": forms.Textarea(attrs={'rows': 4}),
|
||||||
"add_date": forms.DateInput(attrs={'type': 'date'}),
|
"add_date": forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
||||||
"thumbnail_time": forms.NumberInput(attrs={'min': 0, 'step': 0.1}),
|
"thumbnail_time": forms.NumberInput(attrs={'min': 0, 'step': 0.1}),
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
|
|||||||
104
files/frontend_translations/sl.py
Normal file
104
files/frontend_translations/sl.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
translation_strings = {
|
||||||
|
"ABOUT": "O NAS",
|
||||||
|
"AUTOPLAY": "SAMODEJNO PREDVAJANJE",
|
||||||
|
"Add a ": "Dodaj ",
|
||||||
|
"COMMENT": "KOMENTAR",
|
||||||
|
"Categories": "Kategorije",
|
||||||
|
"Category": "Kategorija",
|
||||||
|
"Change Language": "Spremeni jezik",
|
||||||
|
"Change password": "Spremeni geslo",
|
||||||
|
"About": "O nas",
|
||||||
|
"Comment": "Komentar",
|
||||||
|
"Comments": "Komentarji",
|
||||||
|
"Comments are disabled": "Komentarji so onemogočeni",
|
||||||
|
"Contact": "Kontakt",
|
||||||
|
"DELETE MEDIA": "IZBRIŠI MEDIJ",
|
||||||
|
"DOWNLOAD": "PRENESI",
|
||||||
|
"EDIT MEDIA": "UREDI MEDIJ",
|
||||||
|
"EDIT PROFILE": "UREDI PROFIL",
|
||||||
|
"EDIT SUBTITLE": "UREDI PODNAPISE",
|
||||||
|
"Edit media": "Uredi medij",
|
||||||
|
"Edit profile": "Uredi profil",
|
||||||
|
"Edit subtitle": "Uredi podnapise",
|
||||||
|
"Featured": "Izbrani",
|
||||||
|
"Go": "Pojdi",
|
||||||
|
"History": "Zgodovina",
|
||||||
|
"Home": "Domov",
|
||||||
|
"Language": "Jezik",
|
||||||
|
"Latest": "Najnovejši",
|
||||||
|
"Liked media": "Všečkani mediji",
|
||||||
|
"Manage comments": "Upravljaj komentarje",
|
||||||
|
"Manage media": "Upravljaj medije",
|
||||||
|
"Manage users": "Upravljaj uporabnike",
|
||||||
|
"Media": "Mediji",
|
||||||
|
"Media was edited": "Medij je bil urejen",
|
||||||
|
"Members": "Člani",
|
||||||
|
"My media": "Moji mediji",
|
||||||
|
"My playlists": "Moji seznami predvajanja",
|
||||||
|
"No": "Ne",
|
||||||
|
"No comment yet": "Brez komentarja",
|
||||||
|
"No comments yet": "Brez komentarjev",
|
||||||
|
"No results for": "Ni rezultatov za",
|
||||||
|
"PLAYLISTS": "SEZNAMI PREDVAJANJA",
|
||||||
|
"Playlists": "Seznami predvajanja",
|
||||||
|
"Powered by": "Poganja",
|
||||||
|
"Published on": "Objavljeno",
|
||||||
|
"Recommended": "Priporočeno",
|
||||||
|
"Register": "Registracija",
|
||||||
|
"SAVE": "SHRANI",
|
||||||
|
"SEARCH": "ISKANJE",
|
||||||
|
"SHARE": "DELI",
|
||||||
|
"SHOW MORE": "PRIKAŽI VEČ",
|
||||||
|
"SUBMIT": "POŠLJI",
|
||||||
|
"Search": "Iskanje",
|
||||||
|
"Select": "Izberi",
|
||||||
|
"Sign in": "Prijava",
|
||||||
|
"Sign out": "Odjava",
|
||||||
|
"Subtitle was added": "Podnapisi so bili dodani",
|
||||||
|
"Tags": "Oznake",
|
||||||
|
"Terms": "Pogoji",
|
||||||
|
"UPLOAD": "NALOŽI",
|
||||||
|
"Up next": "Naslednji",
|
||||||
|
"Upload": "Naloži",
|
||||||
|
"Upload media": "Naloži medij",
|
||||||
|
"Uploads": "Naloženi",
|
||||||
|
"VIEW ALL": "PRIKAŽI VSE",
|
||||||
|
"View all": "Prikaži vse",
|
||||||
|
"comment": "komentar",
|
||||||
|
"is a modern, fully featured open source video and media CMS. It is developed to meet the needs of modern web platforms for viewing and sharing media": "je moderni, popolnoma opremljen odprtokodni video in medijski CMS. Razvit je za potrebe sodobnih spletnih platform za ogled in deljenje medijev",
|
||||||
|
"media in category": "mediji v kategoriji",
|
||||||
|
"media in tag": "mediji z oznako",
|
||||||
|
"view": "ogled",
|
||||||
|
"views": "ogledi",
|
||||||
|
"yet": "še",
|
||||||
|
}
|
||||||
|
|
||||||
|
replacement_strings = {
|
||||||
|
"Apr": "Apr",
|
||||||
|
"Aug": "Avg",
|
||||||
|
"Dec": "Dec",
|
||||||
|
"Feb": "Feb",
|
||||||
|
"Jan": "Jan",
|
||||||
|
"Jul": "Jul",
|
||||||
|
"Jun": "Jun",
|
||||||
|
"Mar": "Mar",
|
||||||
|
"May": "Maj",
|
||||||
|
"Nov": "Nov",
|
||||||
|
"Oct": "Okt",
|
||||||
|
"Sep": "Sep",
|
||||||
|
"day ago": "dan nazaj",
|
||||||
|
"days ago": "dni nazaj",
|
||||||
|
"hour ago": "ura nazaj",
|
||||||
|
"hours ago": "ur nazaj",
|
||||||
|
"just now": "pravkar",
|
||||||
|
"minute ago": "minuta nazaj",
|
||||||
|
"minutes ago": "minut nazaj",
|
||||||
|
"month ago": "mesec nazaj",
|
||||||
|
"months ago": "mesecev nazaj",
|
||||||
|
"second ago": "sekunda nazaj",
|
||||||
|
"seconds ago": "sekund nazaj",
|
||||||
|
"week ago": "teden nazaj",
|
||||||
|
"weeks ago": "tednov nazaj",
|
||||||
|
"year ago": "leto nazaj",
|
||||||
|
"years ago": "let nazaj",
|
||||||
|
}
|
||||||
@@ -34,12 +34,6 @@ BUF_SIZE_MULTIPLIER = 1.5
|
|||||||
KEYFRAME_DISTANCE = 4
|
KEYFRAME_DISTANCE = 4
|
||||||
KEYFRAME_DISTANCE_MIN = 2
|
KEYFRAME_DISTANCE_MIN = 2
|
||||||
|
|
||||||
# speed presets
|
|
||||||
# see https://trac.ffmpeg.org/wiki/Encode/H.264
|
|
||||||
X26x_PRESET = "medium" # "medium"
|
|
||||||
X265_PRESET = "medium"
|
|
||||||
X26x_PRESET_BIG_HEIGHT = "faster"
|
|
||||||
|
|
||||||
# VP9_SPEED = 1 # between 0 and 4, lower is slower
|
# VP9_SPEED = 1 # between 0 and 4, lower is slower
|
||||||
VP9_SPEED = 2
|
VP9_SPEED = 2
|
||||||
|
|
||||||
@@ -55,6 +49,7 @@ VIDEO_CRFS = {
|
|||||||
VIDEO_BITRATES = {
|
VIDEO_BITRATES = {
|
||||||
"h264": {
|
"h264": {
|
||||||
25: {
|
25: {
|
||||||
|
144: 150,
|
||||||
240: 300,
|
240: 300,
|
||||||
360: 500,
|
360: 500,
|
||||||
480: 1000,
|
480: 1000,
|
||||||
@@ -67,6 +62,7 @@ VIDEO_BITRATES = {
|
|||||||
},
|
},
|
||||||
"h265": {
|
"h265": {
|
||||||
25: {
|
25: {
|
||||||
|
144: 75,
|
||||||
240: 150,
|
240: 150,
|
||||||
360: 275,
|
360: 275,
|
||||||
480: 500,
|
480: 500,
|
||||||
@@ -79,6 +75,7 @@ VIDEO_BITRATES = {
|
|||||||
},
|
},
|
||||||
"vp9": {
|
"vp9": {
|
||||||
25: {
|
25: {
|
||||||
|
144: 75,
|
||||||
240: 150,
|
240: 150,
|
||||||
360: 275,
|
360: 275,
|
||||||
480: 500,
|
480: 500,
|
||||||
@@ -173,7 +170,7 @@ def rm_dir(directory):
|
|||||||
|
|
||||||
def url_from_path(filename):
|
def url_from_path(filename):
|
||||||
# TODO: find a way to preserver http - https ...
|
# TODO: find a way to preserver http - https ...
|
||||||
return "{0}{1}".format(settings.MEDIA_URL, filename.replace(settings.MEDIA_ROOT, ""))
|
return f"{settings.MEDIA_URL}{filename.replace(settings.MEDIA_ROOT, '')}"
|
||||||
|
|
||||||
|
|
||||||
def create_temp_file(suffix=None, dir=settings.TEMP_DIRECTORY):
|
def create_temp_file(suffix=None, dir=settings.TEMP_DIRECTORY):
|
||||||
@@ -488,7 +485,7 @@ def show_file_size(size):
|
|||||||
if size:
|
if size:
|
||||||
size = size / 1000000
|
size = size / 1000000
|
||||||
size = round(size, 1)
|
size = round(size, 1)
|
||||||
size = "{0}MB".format(str(size))
|
size = f"{str(size)}MB"
|
||||||
return size
|
return size
|
||||||
|
|
||||||
|
|
||||||
@@ -596,17 +593,13 @@ def get_base_ffmpeg_command(
|
|||||||
cmd = base_cmd[:]
|
cmd = base_cmd[:]
|
||||||
|
|
||||||
# preset settings
|
# preset settings
|
||||||
|
preset = getattr(settings, "FFMPEG_DEFAULT_PRESET", "medium")
|
||||||
|
|
||||||
if encoder == "libvpx-vp9":
|
if encoder == "libvpx-vp9":
|
||||||
if pass_number == 1:
|
if pass_number == 1:
|
||||||
speed = 4
|
speed = 4
|
||||||
else:
|
else:
|
||||||
speed = VP9_SPEED
|
speed = VP9_SPEED
|
||||||
elif encoder in ["libx264"]:
|
|
||||||
preset = X26x_PRESET
|
|
||||||
elif encoder in ["libx265"]:
|
|
||||||
preset = X265_PRESET
|
|
||||||
if target_height >= 720:
|
|
||||||
preset = X26x_PRESET_BIG_HEIGHT
|
|
||||||
|
|
||||||
if encoder == "libx264":
|
if encoder == "libx264":
|
||||||
level = "4.2" if target_height <= 1080 else "5.2"
|
level = "4.2" if target_height <= 1080 else "5.2"
|
||||||
@@ -730,7 +723,7 @@ def produce_ffmpeg_commands(media_file, media_info, resolution, codec, output_fi
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if media_info.get("video_height") < resolution:
|
if media_info.get("video_height") < resolution:
|
||||||
if resolution not in [240, 360]: # always get these two
|
if resolution not in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# if codec == "h264_baseline":
|
# if codec == "h264_baseline":
|
||||||
|
|||||||
@@ -166,14 +166,14 @@ Media becomes private if it gets reported %s times\n
|
|||||||
)
|
)
|
||||||
|
|
||||||
if settings.ADMINS_NOTIFICATIONS.get("MEDIA_REPORTED", False):
|
if settings.ADMINS_NOTIFICATIONS.get("MEDIA_REPORTED", False):
|
||||||
title = "[{}] - Media was reported".format(settings.PORTAL_NAME)
|
title = f"[{settings.PORTAL_NAME}] - Media was reported"
|
||||||
d = {}
|
d = {}
|
||||||
d["title"] = title
|
d["title"] = title
|
||||||
d["msg"] = msg
|
d["msg"] = msg
|
||||||
d["to"] = settings.ADMIN_EMAIL_LIST
|
d["to"] = settings.ADMIN_EMAIL_LIST
|
||||||
notify_items.append(d)
|
notify_items.append(d)
|
||||||
if settings.USERS_NOTIFICATIONS.get("MEDIA_REPORTED", False):
|
if settings.USERS_NOTIFICATIONS.get("MEDIA_REPORTED", False):
|
||||||
title = "[{}] - Media was reported".format(settings.PORTAL_NAME)
|
title = f"[{settings.PORTAL_NAME}] - Media was reported"
|
||||||
d = {}
|
d = {}
|
||||||
d["title"] = title
|
d["title"] = title
|
||||||
d["msg"] = msg
|
d["msg"] = msg
|
||||||
@@ -182,7 +182,7 @@ Media becomes private if it gets reported %s times\n
|
|||||||
|
|
||||||
if action == "media_added" and media:
|
if action == "media_added" and media:
|
||||||
if settings.ADMINS_NOTIFICATIONS.get("MEDIA_ADDED", False):
|
if settings.ADMINS_NOTIFICATIONS.get("MEDIA_ADDED", False):
|
||||||
title = "[{}] - Media was added".format(settings.PORTAL_NAME)
|
title = f"[{settings.PORTAL_NAME}] - Media was added"
|
||||||
msg = """
|
msg = """
|
||||||
Media %s was added by user %s.
|
Media %s was added by user %s.
|
||||||
""" % (
|
""" % (
|
||||||
@@ -195,7 +195,7 @@ Media %s was added by user %s.
|
|||||||
d["to"] = settings.ADMIN_EMAIL_LIST
|
d["to"] = settings.ADMIN_EMAIL_LIST
|
||||||
notify_items.append(d)
|
notify_items.append(d)
|
||||||
if settings.USERS_NOTIFICATIONS.get("MEDIA_ADDED", False):
|
if settings.USERS_NOTIFICATIONS.get("MEDIA_ADDED", False):
|
||||||
title = "[{}] - Your media was added".format(settings.PORTAL_NAME)
|
title = f"[{settings.PORTAL_NAME}] - Your media was added"
|
||||||
msg = """
|
msg = """
|
||||||
Your media has been added! It will be encoded and will be available soon.
|
Your media has been added! It will be encoded and will be available soon.
|
||||||
URL: %s
|
URL: %s
|
||||||
@@ -339,7 +339,7 @@ def notify_user_on_comment(friendly_token):
|
|||||||
media_url = settings.SSL_FRONTEND_HOST + media.get_absolute_url()
|
media_url = settings.SSL_FRONTEND_HOST + media.get_absolute_url()
|
||||||
|
|
||||||
if user.notification_on_comments:
|
if user.notification_on_comments:
|
||||||
title = "[{}] - A comment was added".format(settings.PORTAL_NAME)
|
title = f"[{settings.PORTAL_NAME}] - A comment was added"
|
||||||
msg = """
|
msg = """
|
||||||
A comment has been added to your media %s .
|
A comment has been added to your media %s .
|
||||||
View it on %s
|
View it on %s
|
||||||
@@ -363,7 +363,7 @@ def notify_user_on_mention(friendly_token, user_mentioned, cleaned_comment):
|
|||||||
media_url = settings.SSL_FRONTEND_HOST + media.get_absolute_url()
|
media_url = settings.SSL_FRONTEND_HOST + media.get_absolute_url()
|
||||||
|
|
||||||
if user.notification_on_comments:
|
if user.notification_on_comments:
|
||||||
title = "[{}] - You were mentioned in a comment".format(settings.PORTAL_NAME)
|
title = f"[{settings.PORTAL_NAME}] - You were mentioned in a comment"
|
||||||
msg = """
|
msg = """
|
||||||
You were mentioned in a comment on %s .
|
You were mentioned in a comment on %s .
|
||||||
View it on %s
|
View it on %s
|
||||||
|
|||||||
17
files/migrations/0010_alter_encodeprofile_resolution.py
Normal file
17
files/migrations/0010_alter_encodeprofile_resolution.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.1.6 on 2025-07-05 11:49
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('files', '0009_alter_media_friendly_token'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='encodeprofile',
|
||||||
|
name='resolution',
|
||||||
|
field=models.IntegerField(blank=True, choices=[(2160, '2160'), (1440, '1440'), (1080, '1080'), (720, '720'), (480, '480'), (360, '360'), (240, '240'), (144, '144')], null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -13,7 +13,8 @@ from django.contrib.postgres.indexes import GinIndex
|
|||||||
from django.contrib.postgres.search import SearchVectorField
|
from django.contrib.postgres.search import SearchVectorField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db import connection, models
|
from django.db import models
|
||||||
|
from django.db.models import Func, Value
|
||||||
from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete
|
from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -72,6 +73,7 @@ ENCODE_RESOLUTIONS = (
|
|||||||
(480, "480"),
|
(480, "480"),
|
||||||
(360, "360"),
|
(360, "360"),
|
||||||
(240, "240"),
|
(240, "240"),
|
||||||
|
(144, "144"),
|
||||||
)
|
)
|
||||||
|
|
||||||
CODECS = (
|
CODECS = (
|
||||||
@@ -90,34 +92,34 @@ def generate_uid():
|
|||||||
|
|
||||||
def original_media_file_path(instance, filename):
|
def original_media_file_path(instance, filename):
|
||||||
"""Helper function to place original media file"""
|
"""Helper function to place original media file"""
|
||||||
file_name = "{0}.{1}".format(instance.uid.hex, helpers.get_file_name(filename))
|
file_name = f"{instance.uid.hex}.{helpers.get_file_name(filename)}"
|
||||||
return settings.MEDIA_UPLOAD_DIR + "user/{0}/{1}".format(instance.user.username, file_name)
|
return settings.MEDIA_UPLOAD_DIR + f"user/{instance.user.username}/{file_name}"
|
||||||
|
|
||||||
|
|
||||||
def encoding_media_file_path(instance, filename):
|
def encoding_media_file_path(instance, filename):
|
||||||
"""Helper function to place encoded media file"""
|
"""Helper function to place encoded media file"""
|
||||||
|
|
||||||
file_name = "{0}.{1}".format(instance.media.uid.hex, helpers.get_file_name(filename))
|
file_name = f"{instance.media.uid.hex}.{helpers.get_file_name(filename)}"
|
||||||
return settings.MEDIA_ENCODING_DIR + "{0}/{1}/{2}".format(instance.profile.id, instance.media.user.username, file_name)
|
return settings.MEDIA_ENCODING_DIR + f"{instance.profile.id}/{instance.media.user.username}/{file_name}"
|
||||||
|
|
||||||
|
|
||||||
def original_thumbnail_file_path(instance, filename):
|
def original_thumbnail_file_path(instance, filename):
|
||||||
"""Helper function to place original media thumbnail file"""
|
"""Helper function to place original media thumbnail file"""
|
||||||
|
|
||||||
return settings.THUMBNAIL_UPLOAD_DIR + "user/{0}/{1}".format(instance.user.username, filename)
|
return settings.THUMBNAIL_UPLOAD_DIR + f"user/{instance.user.username}/{filename}"
|
||||||
|
|
||||||
|
|
||||||
def subtitles_file_path(instance, filename):
|
def subtitles_file_path(instance, filename):
|
||||||
"""Helper function to place subtitle file"""
|
"""Helper function to place subtitle file"""
|
||||||
|
|
||||||
return settings.SUBTITLES_UPLOAD_DIR + "user/{0}/{1}".format(instance.media.user.username, filename)
|
return settings.SUBTITLES_UPLOAD_DIR + f"user/{instance.media.user.username}/{filename}"
|
||||||
|
|
||||||
|
|
||||||
def category_thumb_path(instance, filename):
|
def category_thumb_path(instance, filename):
|
||||||
"""Helper function to place category thumbnail file"""
|
"""Helper function to place category thumbnail file"""
|
||||||
|
|
||||||
file_name = "{0}.{1}".format(instance.uid.hex, helpers.get_file_name(filename))
|
file_name = f"{instance.uid}.{helpers.get_file_name(filename)}"
|
||||||
return settings.MEDIA_UPLOAD_DIR + "categories/{0}".format(file_name)
|
return settings.MEDIA_UPLOAD_DIR + f"categories/{file_name}"
|
||||||
|
|
||||||
|
|
||||||
class Media(models.Model):
|
class Media(models.Model):
|
||||||
@@ -388,8 +390,6 @@ class Media(models.Model):
|
|||||||
search field is used to store SearchVector
|
search field is used to store SearchVector
|
||||||
"""
|
"""
|
||||||
|
|
||||||
db_table = self._meta.db_table
|
|
||||||
|
|
||||||
# first get anything interesting out of the media
|
# first get anything interesting out of the media
|
||||||
# that needs to be search able
|
# that needs to be search able
|
||||||
|
|
||||||
@@ -413,19 +413,8 @@ class Media(models.Model):
|
|||||||
|
|
||||||
text = helpers.clean_query(text)
|
text = helpers.clean_query(text)
|
||||||
|
|
||||||
sql_code = """
|
Media.objects.filter(id=self.id).update(search=Func(Value('simple'), Value(text), function='to_tsvector'))
|
||||||
UPDATE {db_table} SET search = to_tsvector(
|
|
||||||
'{config}', '{text}'
|
|
||||||
) WHERE {db_table}.id = {id}
|
|
||||||
""".format(
|
|
||||||
db_table=db_table, config="simple", text=text, id=self.id
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
with connection.cursor() as cursor:
|
|
||||||
cursor.execute(sql_code)
|
|
||||||
except BaseException:
|
|
||||||
pass # TODO:add log
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def media_init(self):
|
def media_init(self):
|
||||||
@@ -908,7 +897,7 @@ class Media(models.Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
res = {}
|
res = {}
|
||||||
valid_resolutions = [240, 360, 480, 720, 1080, 1440, 2160]
|
valid_resolutions = [144, 240, 360, 480, 720, 1080, 1440, 2160]
|
||||||
if self.hls_file:
|
if self.hls_file:
|
||||||
if os.path.exists(self.hls_file):
|
if os.path.exists(self.hls_file):
|
||||||
hls_file = self.hls_file
|
hls_file = self.hls_file
|
||||||
@@ -925,7 +914,7 @@ class Media(models.Model):
|
|||||||
if resolution not in valid_resolutions:
|
if resolution not in valid_resolutions:
|
||||||
resolution = iframe_playlist.iframe_stream_info.resolution[0]
|
resolution = iframe_playlist.iframe_stream_info.resolution[0]
|
||||||
|
|
||||||
res["{}_iframe".format(resolution)] = helpers.url_from_path(uri)
|
res[f"{resolution}_iframe"] = helpers.url_from_path(uri)
|
||||||
for playlist in m3u8_obj.playlists:
|
for playlist in m3u8_obj.playlists:
|
||||||
uri = os.path.join(p, playlist.uri)
|
uri = os.path.join(p, playlist.uri)
|
||||||
if os.path.exists(uri):
|
if os.path.exists(uri):
|
||||||
@@ -934,7 +923,8 @@ class Media(models.Model):
|
|||||||
if resolution not in valid_resolutions:
|
if resolution not in valid_resolutions:
|
||||||
resolution = playlist.stream_info.resolution[0]
|
resolution = playlist.stream_info.resolution[0]
|
||||||
|
|
||||||
res["{}_playlist".format(resolution)] = helpers.url_from_path(uri)
|
res[f"{resolution}_playlist"] = helpers.url_from_path(uri)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -953,11 +943,11 @@ class Media(models.Model):
|
|||||||
|
|
||||||
def get_absolute_url(self, api=False, edit=False):
|
def get_absolute_url(self, api=False, edit=False):
|
||||||
if edit:
|
if edit:
|
||||||
return reverse("edit_media") + "?m={0}".format(self.friendly_token)
|
return f"{reverse('edit_media')}?m={self.friendly_token}"
|
||||||
if api:
|
if api:
|
||||||
return reverse("api_get_media", kwargs={"friendly_token": self.friendly_token})
|
return reverse("api_get_media", kwargs={"friendly_token": self.friendly_token})
|
||||||
else:
|
else:
|
||||||
return reverse("get_media") + "?m={0}".format(self.friendly_token)
|
return f"{reverse('get_media')}?m={self.friendly_token}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def edit_url(self):
|
def edit_url(self):
|
||||||
@@ -965,7 +955,7 @@ class Media(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def add_subtitle_url(self):
|
def add_subtitle_url(self):
|
||||||
return "/add_subtitle?m=%s" % self.friendly_token
|
return f"/add_subtitle?m={self.friendly_token}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ratings_info(self):
|
def ratings_info(self):
|
||||||
@@ -1060,7 +1050,7 @@ class Category(models.Model):
|
|||||||
verbose_name_plural = "Categories"
|
verbose_name_plural = "Categories"
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("search") + "?c={0}".format(self.title)
|
return f"{reverse('search')}?c={self.title}"
|
||||||
|
|
||||||
def update_category_media(self):
|
def update_category_media(self):
|
||||||
"""Set media_count"""
|
"""Set media_count"""
|
||||||
@@ -1122,7 +1112,7 @@ class Tag(models.Model):
|
|||||||
ordering = ["title"]
|
ordering = ["title"]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("search") + "?t={0}".format(self.title)
|
return f"{reverse('search')}?t={self.title}"
|
||||||
|
|
||||||
def update_tag_media(self):
|
def update_tag_media(self):
|
||||||
self.media_count = Media.objects.filter(state="public", is_reviewed=True, tags=self).count()
|
self.media_count = Media.objects.filter(state="public", is_reviewed=True, tags=self).count()
|
||||||
@@ -1261,7 +1251,7 @@ class Encoding(models.Model):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{0}-{1}".format(self.profile.name, self.media.title)
|
return f"{self.profile.name}-{self.media.title}"
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("api_get_encoding", kwargs={"encoding_id": self.id})
|
return reverse("api_get_encoding", kwargs={"encoding_id": self.id})
|
||||||
@@ -1280,7 +1270,7 @@ class Language(models.Model):
|
|||||||
ordering = ["id"]
|
ordering = ["id"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{0}-{1}".format(self.code, self.title)
|
return f"{self.code}-{self.title}"
|
||||||
|
|
||||||
|
|
||||||
class Subtitle(models.Model):
|
class Subtitle(models.Model):
|
||||||
@@ -1303,7 +1293,7 @@ class Subtitle(models.Model):
|
|||||||
ordering = ["language__title"]
|
ordering = ["language__title"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{0}-{1}".format(self.media.title, self.language.title)
|
return f"{self.media.title}-{self.language.title}"
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return f"{reverse('edit_subtitle')}?id={self.id}"
|
return f"{reverse('edit_subtitle')}?id={self.id}"
|
||||||
@@ -1347,7 +1337,7 @@ class RatingCategory(models.Model):
|
|||||||
verbose_name_plural = "Rating Categories"
|
verbose_name_plural = "Rating Categories"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{0}".format(self.title)
|
return f"{self.title}"
|
||||||
|
|
||||||
|
|
||||||
def validate_rating(value):
|
def validate_rating(value):
|
||||||
@@ -1376,7 +1366,7 @@ class Rating(models.Model):
|
|||||||
unique_together = ("user", "media", "rating_category")
|
unique_together = ("user", "media", "rating_category")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{0}, rate for {1} for category {2}".format(self.user.username, self.media.title, self.rating_category.title)
|
return f"{self.user.username}, rate for {self.media.title} for category {self.rating_category.title}"
|
||||||
|
|
||||||
|
|
||||||
class Playlist(models.Model):
|
class Playlist(models.Model):
|
||||||
@@ -1488,7 +1478,7 @@ class Comment(MPTTModel):
|
|||||||
order_insertion_by = ["add_date"]
|
order_insertion_by = ["add_date"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "On {0} by {1}".format(self.media.title, self.user.username)
|
return f"On {self.media.title} by {self.user.username}"
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
strip_text_items = ["text"]
|
strip_text_items = ["text"]
|
||||||
@@ -1501,7 +1491,7 @@ class Comment(MPTTModel):
|
|||||||
super(Comment, self).save(*args, **kwargs)
|
super(Comment, self).save(*args, **kwargs)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("get_media") + "?m={0}".format(self.media.friendly_token)
|
return f"{reverse('get_media')}?m={self.media.friendly_token}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_url(self):
|
def media_url(self):
|
||||||
@@ -1720,10 +1710,10 @@ def encoding_file_save(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
|
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
|
||||||
seg_file = helpers.create_temp_file(suffix=".txt", dir=temp_dir)
|
seg_file = helpers.create_temp_file(suffix=".txt", dir=temp_dir)
|
||||||
tf = helpers.create_temp_file(suffix=".{0}".format(instance.profile.extension), dir=temp_dir)
|
tf = helpers.create_temp_file(suffix=f".{instance.profile.extension}", dir=temp_dir)
|
||||||
with open(seg_file, "w") as ff:
|
with open(seg_file, "w") as ff:
|
||||||
for f in chunks_paths:
|
for f in chunks_paths:
|
||||||
ff.write("file {}\n".format(f))
|
ff.write(f"file {f}\n")
|
||||||
cmd = [
|
cmd = [
|
||||||
settings.FFMPEG_COMMAND,
|
settings.FFMPEG_COMMAND,
|
||||||
"-y",
|
"-y",
|
||||||
@@ -1750,7 +1740,7 @@ def encoding_file_save(sender, instance, created, **kwargs):
|
|||||||
progress=100,
|
progress=100,
|
||||||
)
|
)
|
||||||
all_logs = "\n".join([st.logs for st in chunks])
|
all_logs = "\n".join([st.logs for st in chunks])
|
||||||
encoding.logs = "{0}\n{1}\n{2}".format(chunks_paths, stdout, all_logs)
|
encoding.logs = f"{chunks_paths}\n{stdout}\n{all_logs}"
|
||||||
workers = list(set([st.worker for st in chunks]))
|
workers = list(set([st.worker for st in chunks]))
|
||||||
encoding.worker = json.dumps({"workers": workers})
|
encoding.worker = json.dumps({"workers": workers})
|
||||||
|
|
||||||
@@ -1761,10 +1751,7 @@ def encoding_file_save(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
with open(tf, "rb") as f:
|
with open(tf, "rb") as f:
|
||||||
myfile = File(f)
|
myfile = File(f)
|
||||||
output_name = "{0}.{1}".format(
|
output_name = f"{helpers.get_file_name(instance.media.media_file.path)}.{instance.profile.extension}"
|
||||||
helpers.get_file_name(instance.media.media_file.path),
|
|
||||||
instance.profile.extension,
|
|
||||||
)
|
|
||||||
encoding.media_file.save(content=myfile, name=output_name)
|
encoding.media_file.save(content=myfile, name=output_name)
|
||||||
|
|
||||||
# encoding is saved, deleting chunks
|
# encoding is saved, deleting chunks
|
||||||
@@ -1803,7 +1790,7 @@ def encoding_file_save(sender, instance, created, **kwargs):
|
|||||||
chunks_paths = [f.media_file.path for f in chunks]
|
chunks_paths = [f.media_file.path for f in chunks]
|
||||||
|
|
||||||
all_logs = "\n".join([st.logs for st in chunks])
|
all_logs = "\n".join([st.logs for st in chunks])
|
||||||
encoding.logs = "{0}\n{1}".format(chunks_paths, all_logs)
|
encoding.logs = f"{chunks_paths}\n{all_logs}"
|
||||||
workers = list(set([st.worker for st in chunks]))
|
workers = list(set([st.worker for st in chunks]))
|
||||||
encoding.worker = json.dumps({"workers": workers})
|
encoding.worker = json.dumps({"workers": workers})
|
||||||
start_date = min([st.add_date for st in chunks])
|
start_date = min([st.add_date for st in chunks])
|
||||||
|
|||||||
@@ -136,8 +136,8 @@ def chunkize_media(self, friendly_token, profiles, force=True):
|
|||||||
cwd = os.path.dirname(os.path.realpath(media.media_file.path))
|
cwd = os.path.dirname(os.path.realpath(media.media_file.path))
|
||||||
file_name = media.media_file.path.split("/")[-1]
|
file_name = media.media_file.path.split("/")[-1]
|
||||||
random_prefix = produce_friendly_token()
|
random_prefix = produce_friendly_token()
|
||||||
file_format = "{0}_{1}".format(random_prefix, file_name)
|
file_format = f"{random_prefix}_{file_name}"
|
||||||
chunks_file_name = "%02d_{0}".format(file_format)
|
chunks_file_name = f"%02d_{file_format}"
|
||||||
chunks_file_name += ".mkv"
|
chunks_file_name += ".mkv"
|
||||||
cmd = [
|
cmd = [
|
||||||
settings.FFMPEG_COMMAND,
|
settings.FFMPEG_COMMAND,
|
||||||
@@ -162,7 +162,7 @@ def chunkize_media(self, friendly_token, profiles, force=True):
|
|||||||
chunks.append(ch[0])
|
chunks.append(ch[0])
|
||||||
if not chunks:
|
if not chunks:
|
||||||
# command completely failed to segment file.putting to normal encode
|
# command completely failed to segment file.putting to normal encode
|
||||||
logger.info("Failed to break file {0} in chunks." " Putting to normal encode queue".format(friendly_token))
|
logger.info(f"Failed to break file {friendly_token} in chunks. Putting to normal encode queue")
|
||||||
for profile in profiles:
|
for profile in profiles:
|
||||||
if media.video_height and media.video_height < profile.resolution:
|
if media.video_height and media.video_height < profile.resolution:
|
||||||
if profile.resolution not in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
|
if profile.resolution not in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
|
||||||
@@ -211,7 +211,7 @@ def chunkize_media(self, friendly_token, profiles, force=True):
|
|||||||
priority=priority,
|
priority=priority,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("got {0} chunks and will encode to {1} profiles".format(len(chunks), to_profiles))
|
logger.info(f"got {len(chunks)} chunks and will encode to {to_profiles} profiles")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -355,8 +355,8 @@ def encode_media(
|
|||||||
# return False
|
# return False
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
|
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
|
||||||
tf = create_temp_file(suffix=".{0}".format(profile.extension), dir=temp_dir)
|
tf = create_temp_file(suffix=f".{profile.extension}", dir=temp_dir)
|
||||||
tfpass = create_temp_file(suffix=".{0}".format(profile.extension), dir=temp_dir)
|
tfpass = create_temp_file(suffix=f".{profile.extension}", dir=temp_dir)
|
||||||
ffmpeg_commands = produce_ffmpeg_commands(
|
ffmpeg_commands = produce_ffmpeg_commands(
|
||||||
original_media_path,
|
original_media_path,
|
||||||
media.media_info,
|
media.media_info,
|
||||||
@@ -398,7 +398,7 @@ def encode_media(
|
|||||||
if n_times % 60 == 0:
|
if n_times % 60 == 0:
|
||||||
encoding.progress = percent
|
encoding.progress = percent
|
||||||
encoding.save(update_fields=["progress", "update_date"])
|
encoding.save(update_fields=["progress", "update_date"])
|
||||||
logger.info("Saved {0}".format(round(percent, 2)))
|
logger.info(f"Saved {round(percent, 2)}")
|
||||||
n_times += 1
|
n_times += 1
|
||||||
except DatabaseError:
|
except DatabaseError:
|
||||||
# primary reason for this is that the encoding has been deleted, because
|
# primary reason for this is that the encoding has been deleted, because
|
||||||
@@ -451,7 +451,7 @@ def encode_media(
|
|||||||
|
|
||||||
with open(tf, "rb") as f:
|
with open(tf, "rb") as f:
|
||||||
myfile = File(f)
|
myfile = File(f)
|
||||||
output_name = "{0}.{1}".format(get_file_name(original_media_path), profile.extension)
|
output_name = f"{get_file_name(original_media_path)}.{profile.extension}"
|
||||||
encoding.media_file.save(content=myfile, name=output_name)
|
encoding.media_file.save(content=myfile, name=output_name)
|
||||||
encoding.total_run_time = (encoding.update_date - encoding.add_date).seconds
|
encoding.total_run_time = (encoding.update_date - encoding.add_date).seconds
|
||||||
|
|
||||||
@@ -472,7 +472,7 @@ def produce_sprite_from_video(friendly_token):
|
|||||||
try:
|
try:
|
||||||
media = Media.objects.get(friendly_token=friendly_token)
|
media = Media.objects.get(friendly_token=friendly_token)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.info("failed to get media with friendly_token %s" % friendly_token)
|
logger.info(f"failed to get media with friendly_token {friendly_token}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as tmpdirname:
|
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as tmpdirname:
|
||||||
@@ -516,7 +516,7 @@ def create_hls(friendly_token):
|
|||||||
try:
|
try:
|
||||||
media = Media.objects.get(friendly_token=friendly_token)
|
media = Media.objects.get(friendly_token=friendly_token)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.info("failed to get media with friendly_token %s" % friendly_token)
|
logger.info(f"failed to get media with friendly_token {friendly_token}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
p = media.uid.hex
|
p = media.uid.hex
|
||||||
@@ -558,7 +558,7 @@ def check_running_states():
|
|||||||
|
|
||||||
encodings = Encoding.objects.filter(status="running")
|
encodings = Encoding.objects.filter(status="running")
|
||||||
|
|
||||||
logger.info("got {0} encodings that are in state running".format(encodings.count()))
|
logger.info(f"got {encodings.count()} encodings that are in state running")
|
||||||
changed = 0
|
changed = 0
|
||||||
for encoding in encodings:
|
for encoding in encodings:
|
||||||
now = datetime.now(encoding.update_date.tzinfo)
|
now = datetime.now(encoding.update_date.tzinfo)
|
||||||
@@ -575,7 +575,7 @@ def check_running_states():
|
|||||||
# TODO: allign with new code + chunksize...
|
# TODO: allign with new code + chunksize...
|
||||||
changed += 1
|
changed += 1
|
||||||
if changed:
|
if changed:
|
||||||
logger.info("changed from running to pending on {0} items".format(changed))
|
logger.info(f"changed from running to pending on {changed} items")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -585,7 +585,7 @@ def check_media_states():
|
|||||||
# check encoding status of not success media
|
# check encoding status of not success media
|
||||||
media = Media.objects.filter(Q(encoding_status="running") | Q(encoding_status="fail") | Q(encoding_status="pending"))
|
media = Media.objects.filter(Q(encoding_status="running") | Q(encoding_status="fail") | Q(encoding_status="pending"))
|
||||||
|
|
||||||
logger.info("got {0} media that are not in state success".format(media.count()))
|
logger.info(f"got {media.count()} media that are not in state success")
|
||||||
|
|
||||||
changed = 0
|
changed = 0
|
||||||
for m in media:
|
for m in media:
|
||||||
@@ -593,7 +593,7 @@ def check_media_states():
|
|||||||
m.save(update_fields=["encoding_status"])
|
m.save(update_fields=["encoding_status"])
|
||||||
changed += 1
|
changed += 1
|
||||||
if changed:
|
if changed:
|
||||||
logger.info("changed encoding status to {0} media items".format(changed))
|
logger.info(f"changed encoding status to {changed} media items")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -628,7 +628,7 @@ def check_pending_states():
|
|||||||
media.encode(profiles=[profile], force=False)
|
media.encode(profiles=[profile], force=False)
|
||||||
changed += 1
|
changed += 1
|
||||||
if changed:
|
if changed:
|
||||||
logger.info("set to the encode queue {0} encodings that were on pending state".format(changed))
|
logger.info(f"set to the encode queue {changed} encodings that were on pending state")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -652,7 +652,7 @@ def check_missing_profiles():
|
|||||||
# if they appear on the meanwhile (eg on a big queue)
|
# if they appear on the meanwhile (eg on a big queue)
|
||||||
changed += 1
|
changed += 1
|
||||||
if changed:
|
if changed:
|
||||||
logger.info("set to the encode queue {0} profiles".format(changed))
|
logger.info(f"set to the encode queue {changed} profiles")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -828,7 +828,7 @@ def update_listings_thumbnails():
|
|||||||
object.save(update_fields=["listings_thumbnail"])
|
object.save(update_fields=["listings_thumbnail"])
|
||||||
used_media.append(media.friendly_token)
|
used_media.append(media.friendly_token)
|
||||||
saved += 1
|
saved += 1
|
||||||
logger.info("updated {} categories".format(saved))
|
logger.info(f"updated {saved} categories")
|
||||||
|
|
||||||
# Tags
|
# Tags
|
||||||
used_media = []
|
used_media = []
|
||||||
@@ -841,7 +841,7 @@ def update_listings_thumbnails():
|
|||||||
object.save(update_fields=["listings_thumbnail"])
|
object.save(update_fields=["listings_thumbnail"])
|
||||||
used_media.append(media.friendly_token)
|
used_media.append(media.friendly_token)
|
||||||
saved += 1
|
saved += 1
|
||||||
logger.info("updated {} tags".format(saved))
|
logger.info(f"updated {saved} tags")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ def contact(request):
|
|||||||
name = request.POST.get("name")
|
name = request.POST.get("name")
|
||||||
message = request.POST.get("message")
|
message = request.POST.get("message")
|
||||||
|
|
||||||
title = "[{}] - Contact form message received".format(settings.PORTAL_NAME)
|
title = f"[{settings.PORTAL_NAME}] - Contact form message received"
|
||||||
|
|
||||||
msg = """
|
msg = """
|
||||||
You have received a message through the contact form\n
|
You have received a message through the contact form\n
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
[{"model": "files.encodeprofile", "pk": 19, "fields": {"name": "h264-2160", "extension": "mp4", "resolution": 2160, "codec": "h264", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 22, "fields": {"name": "vp9-2160", "extension": "webm", "resolution": 2160, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 16, "fields": {"name": "h265-2160", "extension": "mp4", "resolution": 2160, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 4, "fields": {"name": "h264-1440", "extension": "mp4", "resolution": 1440, "codec": "h264", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 5, "fields": {"name": "vp9-1440", "extension": "webm", "resolution": 1440, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 6, "fields": {"name": "h265-1440", "extension": "mp4", "resolution": 1440, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 7, "fields": {"name": "h264-1080", "extension": "mp4", "resolution": 1080, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 8, "fields": {"name": "vp9-1080", "extension": "webm", "resolution": 1080, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 9, "fields": {"name": "h265-1080", "extension": "mp4", "resolution": 1080, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 10, "fields": {"name": "h264-720", "extension": "mp4", "resolution": 720, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 11, "fields": {"name": "vp9-720", "extension": "webm", "resolution": 720, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 12, "fields": {"name": "h265-720", "extension": "mp4", "resolution": 720, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 13, "fields": {"name": "h264-480", "extension": "mp4", "resolution": 480, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 14, "fields": {"name": "vp9-480", "extension": "webm", "resolution": 480, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 15, "fields": {"name": "h265-480", "extension": "mp4", "resolution": 480, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 3, "fields": {"name": "h264-360", "extension": "mp4", "resolution": 360, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 17, "fields": {"name": "vp9-360", "extension": "webm", "resolution": 360, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 18, "fields": {"name": "h265-360", "extension": "mp4", "resolution": 360, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 2, "fields": {"name": "h264-240", "extension": "mp4", "resolution": 240, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 20, "fields": {"name": "vp9-240", "extension": "webm", "resolution": 240, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 21, "fields": {"name": "h265-240", "extension": "mp4", "resolution": 240, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 1, "fields": {"name": "preview", "extension": "gif", "resolution": null, "codec": null, "description": "", "active": true}}]
|
[{"model": "files.encodeprofile", "pk": 19, "fields": {"name": "h264-2160", "extension": "mp4", "resolution": 2160, "codec": "h264", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 22, "fields": {"name": "vp9-2160", "extension": "webm", "resolution": 2160, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 23, "fields": {"name": "h264-144", "extension": "mp4", "resolution": 144, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 16, "fields": {"name": "h265-2160", "extension": "mp4", "resolution": 2160, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 4, "fields": {"name": "h264-1440", "extension": "mp4", "resolution": 1440, "codec": "h264", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 5, "fields": {"name": "vp9-1440", "extension": "webm", "resolution": 1440, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 6, "fields": {"name": "h265-1440", "extension": "mp4", "resolution": 1440, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 7, "fields": {"name": "h264-1080", "extension": "mp4", "resolution": 1080, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 8, "fields": {"name": "vp9-1080", "extension": "webm", "resolution": 1080, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 9, "fields": {"name": "h265-1080", "extension": "mp4", "resolution": 1080, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 10, "fields": {"name": "h264-720", "extension": "mp4", "resolution": 720, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 11, "fields": {"name": "vp9-720", "extension": "webm", "resolution": 720, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 12, "fields": {"name": "h265-720", "extension": "mp4", "resolution": 720, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 13, "fields": {"name": "h264-480", "extension": "mp4", "resolution": 480, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 14, "fields": {"name": "vp9-480", "extension": "webm", "resolution": 480, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 15, "fields": {"name": "h265-480", "extension": "mp4", "resolution": 480, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 3, "fields": {"name": "h264-360", "extension": "mp4", "resolution": 360, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 17, "fields": {"name": "vp9-360", "extension": "webm", "resolution": 360, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 18, "fields": {"name": "h265-360", "extension": "mp4", "resolution": 360, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 2, "fields": {"name": "h264-240", "extension": "mp4", "resolution": 240, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 20, "fields": {"name": "vp9-240", "extension": "webm", "resolution": 240, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 21, "fields": {"name": "h265-240", "extension": "mp4", "resolution": 240, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 1, "fields": {"name": "preview", "extension": "gif", "resolution": null, "codec": null, "description": "", "active": true}}]
|
||||||
|
|||||||
3
frontend-tools/video-editor/.gitignore
vendored
3
frontend-tools/video-editor/.gitignore
vendored
@@ -10,3 +10,6 @@ client/public/videos/sample-video-30s.mp4
|
|||||||
client/public/videos/sample-video-37s.mp4
|
client/public/videos/sample-video-37s.mp4
|
||||||
videos/sample-video-37s.mp4
|
videos/sample-video-37s.mp4
|
||||||
client/public/videos/sample-video-30s.mp4
|
client/public/videos/sample-video-30s.mp4
|
||||||
|
client/public/videos/sample-video-1.mp4
|
||||||
|
client/public/videos/sample-video-10m.mp4
|
||||||
|
client/public/videos/sample-video-10s.mp4
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
*
|
|
||||||
22
frontend-tools/video-editor/.prettierrc
Normal file
22
frontend-tools/video-editor/.prettierrc
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"trailingComma": "none",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"embeddedLanguageFormatting": "auto",
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.css", "*.scss"],
|
||||||
|
"options": {
|
||||||
|
"singleQuote": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
5
frontend-tools/video-editor/.vscode/settings.json
vendored
Normal file
5
frontend-tools/video-editor/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"prettier.configPath": ".prettierrc"
|
||||||
|
}
|
||||||
@@ -129,3 +129,43 @@ npm run deploy
|
|||||||
## API Integration
|
## API Integration
|
||||||
|
|
||||||
The video editor interfaces with MediaCMS through a set of API endpoints for retrieving and saving video edits.
|
The video editor interfaces with MediaCMS through a set of API endpoints for retrieving and saving video edits.
|
||||||
|
|
||||||
|
Sure! Here's your updated `README.md` section with a new **"Code Formatting"** section using Prettier. I placed it after the "Development" section to keep the flow logical:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Formatting
|
||||||
|
|
||||||
|
To automatically format all source files using [Prettier](https://prettier.io):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format all code in the src directory
|
||||||
|
npx prettier --write src/
|
||||||
|
```
|
||||||
|
|
||||||
|
Or for specific file types:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend-tools/video-editor/
|
||||||
|
npx prettier --write "client/src/**/*.{js,jsx,ts,tsx,json,css,scss,md}"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also add this as a script in `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"scripts": {
|
||||||
|
"format": "prettier --write client/src/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn format
|
||||||
|
# or
|
||||||
|
npm run format
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Let me know if you'd like to auto-format on commit using `lint-staged` + `husky`.
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ const App = () => {
|
|||||||
isPlaying,
|
isPlaying,
|
||||||
setIsPlaying,
|
setIsPlaying,
|
||||||
isMuted,
|
isMuted,
|
||||||
isPreviewMode,
|
|
||||||
thumbnails,
|
thumbnails,
|
||||||
trimStart,
|
trimStart,
|
||||||
trimEnd,
|
trimEnd,
|
||||||
@@ -34,7 +33,6 @@ const App = () => {
|
|||||||
handleReset,
|
handleReset,
|
||||||
handleUndo,
|
handleUndo,
|
||||||
handleRedo,
|
handleRedo,
|
||||||
handlePreview,
|
|
||||||
toggleMute,
|
toggleMute,
|
||||||
handleSave,
|
handleSave,
|
||||||
handleSaveACopy,
|
handleSaveACopy,
|
||||||
@@ -43,7 +41,7 @@ const App = () => {
|
|||||||
videoInitialized,
|
videoInitialized,
|
||||||
setVideoInitialized,
|
setVideoInitialized,
|
||||||
isPlayingSegments,
|
isPlayingSegments,
|
||||||
handlePlaySegments,
|
handlePlaySegments
|
||||||
} = useVideoTrimmer();
|
} = useVideoTrimmer();
|
||||||
|
|
||||||
// Function to play from the beginning
|
// Function to play from the beginning
|
||||||
@@ -92,7 +90,7 @@ const App = () => {
|
|||||||
const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
||||||
|
|
||||||
// First, check if we're inside a segment or exactly at its start/end
|
// First, check if we're inside a segment or exactly at its start/end
|
||||||
currentSegment = sortedSegments.find(seg => {
|
currentSegment = sortedSegments.find((seg) => {
|
||||||
const segStartTime = Number(seg.startTime.toFixed(6));
|
const segStartTime = Number(seg.startTime.toFixed(6));
|
||||||
const segEndTime = Number(seg.endTime.toFixed(6));
|
const segEndTime = Number(seg.endTime.toFixed(6));
|
||||||
|
|
||||||
@@ -114,7 +112,7 @@ const App = () => {
|
|||||||
|
|
||||||
// If we're not in a segment, find the next segment
|
// If we're not in a segment, find the next segment
|
||||||
if (!currentSegment) {
|
if (!currentSegment) {
|
||||||
nextSegment = sortedSegments.find(seg => {
|
nextSegment = sortedSegments.find((seg) => {
|
||||||
const segStartTime = Number(seg.startTime.toFixed(6));
|
const segStartTime = Number(seg.startTime.toFixed(6));
|
||||||
return segStartTime > currentPosition;
|
return segStartTime > currentPosition;
|
||||||
});
|
});
|
||||||
@@ -173,24 +171,32 @@ const App = () => {
|
|||||||
setTimeout(setExactPosition, 50); // Final verification
|
setTimeout(setExactPosition, 50); // Final verification
|
||||||
|
|
||||||
// Remove our boundary checker
|
// Remove our boundary checker
|
||||||
video.removeEventListener('timeupdate', checkBoundary);
|
video.removeEventListener("timeupdate", checkBoundary);
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
|
|
||||||
// Log the final position for debugging
|
// Log the final position for debugging
|
||||||
logger.debug("Stopped at position:", {
|
logger.debug("Stopped at position:", {
|
||||||
target: formatDetailedTime(stopTime),
|
target: formatDetailedTime(stopTime),
|
||||||
actual: formatDetailedTime(video.currentTime),
|
actual: formatDetailedTime(video.currentTime),
|
||||||
type: currentSegment ? "segment end" : (nextSegment ? "next segment start" : "end of video"),
|
type: currentSegment
|
||||||
segment: currentSegment ? {
|
? "segment end"
|
||||||
|
: nextSegment
|
||||||
|
? "next segment start"
|
||||||
|
: "end of video",
|
||||||
|
segment: currentSegment
|
||||||
|
? {
|
||||||
id: currentSegment.id,
|
id: currentSegment.id,
|
||||||
start: formatDetailedTime(currentSegment.startTime),
|
start: formatDetailedTime(currentSegment.startTime),
|
||||||
end: formatDetailedTime(currentSegment.endTime)
|
end: formatDetailedTime(currentSegment.endTime)
|
||||||
} : null,
|
}
|
||||||
nextSegment: nextSegment ? {
|
: null,
|
||||||
|
nextSegment: nextSegment
|
||||||
|
? {
|
||||||
id: nextSegment.id,
|
id: nextSegment.id,
|
||||||
start: formatDetailedTime(nextSegment.startTime),
|
start: formatDetailedTime(nextSegment.startTime),
|
||||||
end: formatDetailedTime(nextSegment.endTime)
|
end: formatDetailedTime(nextSegment.endTime)
|
||||||
} : null
|
}
|
||||||
|
: null
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -198,39 +204,41 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Start our boundary checker
|
// Start our boundary checker
|
||||||
video.addEventListener('timeupdate', checkBoundary);
|
video.addEventListener("timeupdate", checkBoundary);
|
||||||
|
|
||||||
// Start playing
|
// Start playing
|
||||||
video.play()
|
video
|
||||||
|
.play()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
setVideoInitialized(true);
|
setVideoInitialized(true);
|
||||||
logger.debug("Playback started:", {
|
logger.debug("Playback started:", {
|
||||||
from: formatDetailedTime(currentPosition),
|
from: formatDetailedTime(currentPosition),
|
||||||
to: formatDetailedTime(stopTime),
|
to: formatDetailedTime(stopTime),
|
||||||
currentSegment: currentSegment ? {
|
currentSegment: currentSegment
|
||||||
|
? {
|
||||||
id: currentSegment.id,
|
id: currentSegment.id,
|
||||||
start: formatDetailedTime(currentSegment.startTime),
|
start: formatDetailedTime(currentSegment.startTime),
|
||||||
end: formatDetailedTime(currentSegment.endTime)
|
end: formatDetailedTime(currentSegment.endTime)
|
||||||
} : 'None',
|
}
|
||||||
nextSegment: nextSegment ? {
|
: "None",
|
||||||
|
nextSegment: nextSegment
|
||||||
|
? {
|
||||||
id: nextSegment.id,
|
id: nextSegment.id,
|
||||||
start: formatDetailedTime(nextSegment.startTime),
|
start: formatDetailedTime(nextSegment.startTime),
|
||||||
end: formatDetailedTime(nextSegment.endTime)
|
end: formatDetailedTime(nextSegment.endTime)
|
||||||
} : 'None'
|
}
|
||||||
|
: "None"
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.error("Error playing video:", err);
|
console.error("Error playing video:", err);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background min-h-screen">
|
<div className="bg-background min-h-screen">
|
||||||
<MobilePlayPrompt
|
<MobilePlayPrompt videoRef={videoRef} onPlay={handlePlay} />
|
||||||
videoRef={videoRef}
|
|
||||||
onPlay={handlePlay}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="container mx-auto px-4 py-6 max-w-6xl">
|
<div className="container mx-auto px-4 py-6 max-w-6xl">
|
||||||
{/* Video Player */}
|
{/* Video Player */}
|
||||||
@@ -251,10 +259,8 @@ const App = () => {
|
|||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
onUndo={handleUndo}
|
onUndo={handleUndo}
|
||||||
onRedo={handleRedo}
|
onRedo={handleRedo}
|
||||||
onPreview={handlePreview}
|
|
||||||
onPlaySegments={handlePlaySegments}
|
onPlaySegments={handlePlaySegments}
|
||||||
onPlay={handlePlay}
|
onPlay={handlePlay}
|
||||||
isPreviewMode={isPreviewMode}
|
|
||||||
isPlaying={isPlaying}
|
isPlaying={isPlaying}
|
||||||
isPlayingSegments={isPlayingSegments}
|
isPlayingSegments={isPlayingSegments}
|
||||||
canUndo={historyPosition > 0}
|
canUndo={historyPosition > 0}
|
||||||
@@ -279,7 +285,6 @@ const App = () => {
|
|||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onSaveACopy={handleSaveACopy}
|
onSaveACopy={handleSaveACopy}
|
||||||
onSaveSegments={handleSaveSegments}
|
onSaveSegments={handleSaveSegments}
|
||||||
isPreviewMode={isPreviewMode}
|
|
||||||
hasUnsavedChanges={hasUnsavedChanges}
|
hasUnsavedChanges={hasUnsavedChanges}
|
||||||
isIOSUninitialized={isMobile && !videoInitialized}
|
isIOSUninitialized={isMobile && !videoInitialized}
|
||||||
isPlaying={isPlaying}
|
isPlaying={isPlaying}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { formatTime, formatLongTime } from "@/lib/timeUtils";
|
import { formatTime, formatLongTime } from "@/lib/timeUtils";
|
||||||
import '../styles/ClipSegments.css';
|
import "../styles/ClipSegments.css";
|
||||||
|
|
||||||
export interface Segment {
|
export interface Segment {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -20,7 +20,7 @@ const ClipSegments = ({ segments }: ClipSegmentsProps) => {
|
|||||||
// Handle delete segment click
|
// Handle delete segment click
|
||||||
const handleDeleteSegment = (segmentId: number) => {
|
const handleDeleteSegment = (segmentId: number) => {
|
||||||
// Create and dispatch the delete event
|
// Create and dispatch the delete event
|
||||||
const deleteEvent = new CustomEvent('delete-segment', {
|
const deleteEvent = new CustomEvent("delete-segment", {
|
||||||
detail: { segmentId }
|
detail: { segmentId }
|
||||||
});
|
});
|
||||||
document.dispatchEvent(deleteEvent);
|
document.dispatchEvent(deleteEvent);
|
||||||
@@ -38,19 +38,14 @@ const ClipSegments = ({ segments }: ClipSegmentsProps) => {
|
|||||||
<h3 className="clip-segments-title">Clip Segments</h3>
|
<h3 className="clip-segments-title">Clip Segments</h3>
|
||||||
|
|
||||||
{sortedSegments.map((segment, index) => (
|
{sortedSegments.map((segment, index) => (
|
||||||
<div
|
<div key={segment.id} className={`segment-item ${getSegmentColorClass(index)}`}>
|
||||||
key={segment.id}
|
|
||||||
className={`segment-item ${getSegmentColorClass(index)}`}
|
|
||||||
>
|
|
||||||
<div className="segment-content">
|
<div className="segment-content">
|
||||||
<div
|
<div
|
||||||
className="segment-thumbnail"
|
className="segment-thumbnail"
|
||||||
style={{ backgroundImage: `url(${segment.thumbnail})` }}
|
style={{ backgroundImage: `url(${segment.thumbnail})` }}
|
||||||
></div>
|
></div>
|
||||||
<div className="segment-info">
|
<div className="segment-info">
|
||||||
<div className="segment-title">
|
<div className="segment-title">Segment {index + 1}</div>
|
||||||
Segment {index + 1}
|
|
||||||
</div>
|
|
||||||
<div className="segment-time">
|
<div className="segment-time">
|
||||||
{formatTime(segment.startTime)} - {formatTime(segment.endTime)}
|
{formatTime(segment.startTime)} - {formatTime(segment.endTime)}
|
||||||
</div>
|
</div>
|
||||||
@@ -67,7 +62,11 @@ const ClipSegments = ({ segments }: ClipSegmentsProps) => {
|
|||||||
onClick={() => handleDeleteSegment(segment.id)}
|
onClick={() => handleDeleteSegment(segment.id)}
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import '../styles/EditingTools.css';
|
import "../styles/EditingTools.css";
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
interface EditingToolsProps {
|
interface EditingToolsProps {
|
||||||
onSplit: () => void;
|
onSplit: () => void;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
onUndo: () => void;
|
onUndo: () => void;
|
||||||
onRedo: () => void;
|
onRedo: () => void;
|
||||||
onPreview: () => void;
|
|
||||||
onPlaySegments: () => void;
|
onPlaySegments: () => void;
|
||||||
onPlay: () => void;
|
onPlay: () => void;
|
||||||
canUndo: boolean;
|
canUndo: boolean;
|
||||||
canRedo: boolean;
|
canRedo: boolean;
|
||||||
isPreviewMode?: boolean;
|
|
||||||
isPlaying?: boolean;
|
isPlaying?: boolean;
|
||||||
isPlayingSegments?: boolean;
|
isPlayingSegments?: boolean;
|
||||||
}
|
}
|
||||||
@@ -21,14 +19,12 @@ const EditingTools = ({
|
|||||||
onReset,
|
onReset,
|
||||||
onUndo,
|
onUndo,
|
||||||
onRedo,
|
onRedo,
|
||||||
onPreview,
|
|
||||||
onPlaySegments,
|
onPlaySegments,
|
||||||
onPlay,
|
onPlay,
|
||||||
canUndo,
|
canUndo,
|
||||||
canRedo,
|
canRedo,
|
||||||
isPreviewMode = false,
|
|
||||||
isPlaying = false,
|
isPlaying = false,
|
||||||
isPlayingSegments = false,
|
isPlayingSegments = false
|
||||||
}: EditingToolsProps) => {
|
}: EditingToolsProps) => {
|
||||||
const [isSmallScreen, setIsSmallScreen] = useState(false);
|
const [isSmallScreen, setIsSmallScreen] = useState(false);
|
||||||
|
|
||||||
@@ -38,14 +34,14 @@ const EditingTools = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
checkScreenSize();
|
checkScreenSize();
|
||||||
window.addEventListener('resize', checkScreenSize);
|
window.addEventListener("resize", checkScreenSize);
|
||||||
return () => window.removeEventListener('resize', checkScreenSize);
|
return () => window.removeEventListener("resize", checkScreenSize);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle play button click with iOS fix
|
// Handle play button click with iOS fix
|
||||||
const handlePlay = () => {
|
const handlePlay = () => {
|
||||||
// Ensure lastSeekedPosition is used when play is clicked
|
// Ensure lastSeekedPosition is used when play is clicked
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
console.log("Play button clicked, current lastSeekedPosition:", window.lastSeekedPosition);
|
console.log("Play button clicked, current lastSeekedPosition:", window.lastSeekedPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,12 +58,22 @@ const EditingTools = ({
|
|||||||
<button
|
<button
|
||||||
className={`button segments-button`}
|
className={`button segments-button`}
|
||||||
onClick={onPlaySegments}
|
onClick={onPlaySegments}
|
||||||
data-tooltip={isPlayingSegments ? "Stop segments playback" : "Play segments in one continuous flow"}
|
data-tooltip={
|
||||||
style={{ fontSize: '0.875rem' }}
|
isPlayingSegments ? "Stop segments playback" : "Play segments in one continuous flow"
|
||||||
|
}
|
||||||
|
style={{ fontSize: "0.875rem" }}
|
||||||
>
|
>
|
||||||
{isPlayingSegments ? (
|
{isPlayingSegments ? (
|
||||||
<>
|
<>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
<circle cx="12" cy="12" r="10" />
|
<circle cx="12" cy="12" r="10" />
|
||||||
<line x1="10" y1="15" x2="10" y2="9" />
|
<line x1="10" y1="15" x2="10" y2="9" />
|
||||||
<line x1="14" y1="15" x2="14" y2="9" />
|
<line x1="14" y1="15" x2="14" y2="9" />
|
||||||
@@ -77,7 +83,15 @@ const EditingTools = ({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
<circle cx="12" cy="12" r="10" />
|
<circle cx="12" cy="12" r="10" />
|
||||||
<polygon points="10 8 16 12 10 16 10 8" />
|
<polygon points="10 8 16 12 10 16 10 8" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -116,18 +130,26 @@ const EditingTools = ({
|
|||||||
)}
|
)}
|
||||||
</button> */}
|
</button> */}
|
||||||
|
|
||||||
{/* Standard Play button (only shown when not in preview mode or segments playback) */}
|
{/* Standard Play button (only shown when not in segments playback on small screens) */}
|
||||||
{!isPreviewMode && (!isPlayingSegments || !isSmallScreen) && (
|
{(!isPlayingSegments || !isSmallScreen) && (
|
||||||
<button
|
<button
|
||||||
className={`button play-button ${isPlayingSegments ? 'greyed-out' : ''}`}
|
className={`button play-button ${isPlayingSegments ? "greyed-out" : ""}`}
|
||||||
onClick={handlePlay}
|
onClick={handlePlay}
|
||||||
data-tooltip={isPlaying ? "Pause video" : "Play full video"}
|
data-tooltip={isPlaying ? "Pause video" : "Play full video"}
|
||||||
style={{ fontSize: '0.875rem' }}
|
style={{ fontSize: "0.875rem" }}
|
||||||
disabled={isPlayingSegments}
|
disabled={isPlayingSegments}
|
||||||
>
|
>
|
||||||
{isPlaying ? (
|
{isPlaying && !isPlayingSegments ? (
|
||||||
<>
|
<>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
<circle cx="12" cy="12" r="10" />
|
<circle cx="12" cy="12" r="10" />
|
||||||
<line x1="10" y1="15" x2="10" y2="9" />
|
<line x1="10" y1="15" x2="10" y2="9" />
|
||||||
<line x1="14" y1="15" x2="14" y2="9" />
|
<line x1="14" y1="15" x2="14" y2="9" />
|
||||||
@@ -137,7 +159,15 @@ const EditingTools = ({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
<circle cx="12" cy="12" r="10" />
|
<circle cx="12" cy="12" r="10" />
|
||||||
<polygon points="10 8 16 12 10 16 10 8" />
|
<polygon points="10 8 16 12 10 16 10 8" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -178,26 +208,42 @@ const EditingTools = ({
|
|||||||
<button
|
<button
|
||||||
className="button"
|
className="button"
|
||||||
aria-label="Undo"
|
aria-label="Undo"
|
||||||
data-tooltip="Undo last action"
|
data-tooltip={isPlayingSegments ? "Disabled during preview" : "Undo last action"}
|
||||||
disabled={!canUndo}
|
disabled={!canUndo || isPlayingSegments}
|
||||||
onClick={onUndo}
|
onClick={onUndo}
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg
|
||||||
<path d="M9 14 4 9l5-5"/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5v0a5.5 5.5 0 0 1-5.5 5.5H11"/>
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M9 14 4 9l5-5" />
|
||||||
|
<path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5v0a5.5 5.5 0 0 1-5.5 5.5H11" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="button-text">Undo</span>
|
<span className="button-text">Undo</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="button"
|
className="button"
|
||||||
aria-label="Redo"
|
aria-label="Redo"
|
||||||
data-tooltip="Redo last undone action"
|
data-tooltip={isPlayingSegments ? "Disabled during preview" : "Redo last undone action"}
|
||||||
disabled={!canRedo}
|
disabled={!canRedo || isPlayingSegments}
|
||||||
onClick={onRedo}
|
onClick={onRedo}
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg
|
||||||
<path d="m15 14 5-5-5-5"/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5v0A5.5 5.5 0 0 0 9.5 20H13"/>
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="m15 14 5-5-5-5" />
|
||||||
|
<path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5v0A5.5 5.5 0 0 0 9.5 20H13" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="button-text">Redo</span>
|
<span className="button-text">Redo</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -205,10 +251,15 @@ const EditingTools = ({
|
|||||||
<button
|
<button
|
||||||
className="button"
|
className="button"
|
||||||
onClick={onReset}
|
onClick={onReset}
|
||||||
data-tooltip="Reset to full video"
|
data-tooltip={isPlayingSegments ? "Disabled during preview" : "Reset to full video"}
|
||||||
|
disabled={isPlayingSegments}
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path fillRule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clipRule="evenodd" />
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="reset-text">Reset</span>
|
<span className="reset-text">Reset</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import '../styles/IOSPlayPrompt.css';
|
import "../styles/IOSPlayPrompt.css";
|
||||||
|
|
||||||
interface MobilePlayPromptProps {
|
interface MobilePlayPromptProps {
|
||||||
videoRef: React.RefObject<HTMLVideoElement>;
|
videoRef: React.RefObject<HTMLVideoElement>;
|
||||||
@@ -13,7 +13,9 @@ const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay })
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkIsMobile = () => {
|
const checkIsMobile = () => {
|
||||||
// More comprehensive check for mobile/tablet devices
|
// More comprehensive check for mobile/tablet devices
|
||||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(navigator.userAgent);
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(
|
||||||
|
navigator.userAgent
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Always show for mobile devices on each visit
|
// Always show for mobile devices on each visit
|
||||||
@@ -31,9 +33,9 @@ const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay })
|
|||||||
setIsVisible(false);
|
setIsVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
video.addEventListener('play', handlePlay);
|
video.addEventListener("play", handlePlay);
|
||||||
return () => {
|
return () => {
|
||||||
video.removeEventListener('play', handlePlay);
|
video.removeEventListener("play", handlePlay);
|
||||||
};
|
};
|
||||||
}, [videoRef]);
|
}, [videoRef]);
|
||||||
|
|
||||||
@@ -63,10 +65,7 @@ const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay })
|
|||||||
</ol>
|
</ol>
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
<button
|
<button className="mobile-play-button" onClick={handlePlayClick}>
|
||||||
className="mobile-play-button"
|
|
||||||
onClick={handlePlayClick}
|
|
||||||
>
|
|
||||||
Click to start editing...
|
Click to start editing...
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import { formatTime } from "@/lib/timeUtils";
|
import { formatTime } from "@/lib/timeUtils";
|
||||||
import '../styles/IOSVideoPlayer.css';
|
import "../styles/IOSVideoPlayer.css";
|
||||||
|
|
||||||
interface IOSVideoPlayerProps {
|
interface IOSVideoPlayerProps {
|
||||||
videoRef: React.RefObject<HTMLVideoElement>;
|
videoRef: React.RefObject<HTMLVideoElement>;
|
||||||
@@ -8,11 +8,7 @@ interface IOSVideoPlayerProps {
|
|||||||
duration: number;
|
duration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IOSVideoPlayer = ({
|
const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => {
|
||||||
videoRef,
|
|
||||||
currentTime,
|
|
||||||
duration,
|
|
||||||
}: IOSVideoPlayerProps) => {
|
|
||||||
const [videoUrl, setVideoUrl] = useState<string>("");
|
const [videoUrl, setVideoUrl] = useState<string>("");
|
||||||
const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
|
const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
|
||||||
|
|
||||||
@@ -30,14 +26,14 @@ const IOSVideoPlayer = ({
|
|||||||
|
|
||||||
// Get the video source URL from the main player
|
// Get the video source URL from the main player
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (videoRef.current && videoRef.current.querySelector('source')) {
|
if (videoRef.current && videoRef.current.querySelector("source")) {
|
||||||
const source = videoRef.current.querySelector('source') as HTMLSourceElement;
|
const source = videoRef.current.querySelector("source") as HTMLSourceElement;
|
||||||
if (source && source.src) {
|
if (source && source.src) {
|
||||||
setVideoUrl(source.src);
|
setVideoUrl(source.src);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback to sample video if needed
|
// Fallback to sample video if needed
|
||||||
setVideoUrl("/videos/sample-video-37s.mp4");
|
setVideoUrl("/videos/sample-video-10m.mp4");
|
||||||
}
|
}
|
||||||
}, [videoRef]);
|
}, [videoRef]);
|
||||||
|
|
||||||
@@ -115,12 +111,14 @@ const IOSVideoPlayer = ({
|
|||||||
<div className="ios-video-player-container">
|
<div className="ios-video-player-container">
|
||||||
{/* Current Time / Duration Display */}
|
{/* Current Time / Duration Display */}
|
||||||
<div className="ios-time-display mb-2">
|
<div className="ios-time-display mb-2">
|
||||||
<span className="text-sm">{formatTime(currentTime)} / {formatTime(duration)}</span>
|
<span className="text-sm">
|
||||||
|
{formatTime(currentTime)} / {formatTime(duration)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* iOS-optimized Video Element with Native Controls */}
|
{/* iOS-optimized Video Element with Native Controls */}
|
||||||
<video
|
<video
|
||||||
ref={ref => setIosVideoRef(ref)}
|
ref={(ref) => setIosVideoRef(ref)}
|
||||||
className="w-full rounded-md"
|
className="w-full rounded-md"
|
||||||
src={videoUrl}
|
src={videoUrl}
|
||||||
controls
|
controls
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from "react";
|
||||||
import '../styles/Modal.css';
|
import "../styles/Modal.css";
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -9,31 +9,25 @@ interface ModalProps {
|
|||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Modal: React.FC<ModalProps> = ({
|
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children, actions }) => {
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
actions
|
|
||||||
}) => {
|
|
||||||
// Close modal when Escape key is pressed
|
// Close modal when Escape key is pressed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleEscapeKey = (event: KeyboardEvent) => {
|
const handleEscapeKey = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Escape' && isOpen) {
|
if (event.key === "Escape" && isOpen) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('keydown', handleEscapeKey);
|
document.addEventListener("keydown", handleEscapeKey);
|
||||||
|
|
||||||
// Disable body scrolling when modal is open
|
// Disable body scrolling when modal is open
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = "hidden";
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', handleEscapeKey);
|
document.removeEventListener("keydown", handleEscapeKey);
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = "";
|
||||||
};
|
};
|
||||||
}, [isOpen, onClose]);
|
}, [isOpen, onClose]);
|
||||||
|
|
||||||
@@ -48,14 +42,10 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal-overlay" onClick={handleClickOutside}>
|
<div className="modal-overlay" onClick={handleClickOutside}>
|
||||||
<div className="modal-container" onClick={e => e.stopPropagation()}>
|
<div className="modal-container" onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<h2 className="modal-title">{title}</h2>
|
<h2 className="modal-title">{title}</h2>
|
||||||
<button
|
<button className="modal-close-button" onClick={onClose} aria-label="Close modal">
|
||||||
className="modal-close-button"
|
|
||||||
onClick={onClose}
|
|
||||||
aria-label="Close modal"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="24"
|
width="24"
|
||||||
@@ -73,15 +63,9 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="modal-content">
|
<div className="modal-content">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{actions && (
|
{actions && <div className="modal-actions">{actions}</div>}
|
||||||
<div className="modal-actions">
|
|
||||||
{actions}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
import React, { useRef, useEffect, useState } from "react";
|
import React, { useRef, useEffect, useState } from "react";
|
||||||
import { formatTime, formatDetailedTime } from "@/lib/timeUtils";
|
import { formatTime, formatDetailedTime } from "@/lib/timeUtils";
|
||||||
import logger from '../lib/logger';
|
import logger from "../lib/logger";
|
||||||
import '../styles/VideoPlayer.css';
|
import "../styles/VideoPlayer.css";
|
||||||
|
|
||||||
interface VideoPlayerProps {
|
interface VideoPlayerProps {
|
||||||
videoRef: React.RefObject<HTMLVideoElement>;
|
videoRef: React.RefObject<HTMLVideoElement>;
|
||||||
@@ -33,9 +33,9 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
const [tooltipPosition, setTooltipPosition] = useState({ x: 0 });
|
const [tooltipPosition, setTooltipPosition] = useState({ x: 0 });
|
||||||
const [tooltipTime, setTooltipTime] = useState(0);
|
const [tooltipTime, setTooltipTime] = useState(0);
|
||||||
|
|
||||||
const sampleVideoUrl = typeof window !== 'undefined' &&
|
const sampleVideoUrl =
|
||||||
(window as any).MEDIA_DATA?.videoUrl ||
|
(typeof window !== "undefined" && (window as any).MEDIA_DATA?.videoUrl) ||
|
||||||
"/videos/sample-video-37s.mp4";
|
"/videos/sample-video-10m.mp4";
|
||||||
|
|
||||||
// Detect iOS device
|
// Detect iOS device
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -47,8 +47,8 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
setIsIOS(checkIOS());
|
setIsIOS(checkIOS());
|
||||||
|
|
||||||
// Check if video was previously initialized
|
// Check if video was previously initialized
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
const wasInitialized = localStorage.getItem('video_initialized') === 'true';
|
const wasInitialized = localStorage.getItem("video_initialized") === "true";
|
||||||
setHasInitialized(wasInitialized);
|
setHasInitialized(wasInitialized);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -57,8 +57,8 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPlaying && !hasInitialized) {
|
if (isPlaying && !hasInitialized) {
|
||||||
setHasInitialized(true);
|
setHasInitialized(true);
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
localStorage.setItem('video_initialized', 'true');
|
localStorage.setItem("video_initialized", "true");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isPlaying, hasInitialized]);
|
}, [isPlaying, hasInitialized]);
|
||||||
@@ -70,15 +70,15 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
|
|
||||||
// These attributes need to be set directly on the DOM element
|
// These attributes need to be set directly on the DOM element
|
||||||
// for iOS Safari to respect inline playback
|
// for iOS Safari to respect inline playback
|
||||||
video.setAttribute('playsinline', 'true');
|
video.setAttribute("playsinline", "true");
|
||||||
video.setAttribute('webkit-playsinline', 'true');
|
video.setAttribute("webkit-playsinline", "true");
|
||||||
video.setAttribute('x-webkit-airplay', 'allow');
|
video.setAttribute("x-webkit-airplay", "allow");
|
||||||
|
|
||||||
// Store the last known good position for iOS
|
// Store the last known good position for iOS
|
||||||
const handleTimeUpdate = () => {
|
const handleTimeUpdate = () => {
|
||||||
if (!isDraggingProgressRef.current) {
|
if (!isDraggingProgressRef.current) {
|
||||||
setLastPosition(video.currentTime);
|
setLastPosition(video.currentTime);
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
window.lastSeekedPosition = video.currentTime;
|
window.lastSeekedPosition = video.currentTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,25 +86,25 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
|
|
||||||
// Handle iOS-specific play/pause state
|
// Handle iOS-specific play/pause state
|
||||||
const handlePlay = () => {
|
const handlePlay = () => {
|
||||||
logger.debug('Video play event fired');
|
logger.debug("Video play event fired");
|
||||||
if (isIOS) {
|
if (isIOS) {
|
||||||
setHasInitialized(true);
|
setHasInitialized(true);
|
||||||
localStorage.setItem('video_initialized', 'true');
|
localStorage.setItem("video_initialized", "true");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePause = () => {
|
const handlePause = () => {
|
||||||
logger.debug('Video pause event fired');
|
logger.debug("Video pause event fired");
|
||||||
};
|
};
|
||||||
|
|
||||||
video.addEventListener('timeupdate', handleTimeUpdate);
|
video.addEventListener("timeupdate", handleTimeUpdate);
|
||||||
video.addEventListener('play', handlePlay);
|
video.addEventListener("play", handlePlay);
|
||||||
video.addEventListener('pause', handlePause);
|
video.addEventListener("pause", handlePause);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
video.removeEventListener('timeupdate', handleTimeUpdate);
|
video.removeEventListener("timeupdate", handleTimeUpdate);
|
||||||
video.removeEventListener('play', handlePlay);
|
video.removeEventListener("play", handlePlay);
|
||||||
video.removeEventListener('pause', handlePause);
|
video.removeEventListener("pause", handlePause);
|
||||||
};
|
};
|
||||||
}, [videoRef, isIOS, isDraggingProgressRef]);
|
}, [videoRef, isIOS, isDraggingProgressRef]);
|
||||||
|
|
||||||
@@ -150,12 +150,12 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
setIsDraggingProgress(false);
|
setIsDraggingProgress(false);
|
||||||
isDraggingProgressRef.current = false;
|
isDraggingProgressRef.current = false;
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle progress dragging for both mouse and touch events
|
// Handle progress dragging for both mouse and touch events
|
||||||
@@ -174,7 +174,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
setLastPosition(seekTime);
|
setLastPosition(seekTime);
|
||||||
|
|
||||||
// Also store globally for integration with other components
|
// Also store globally for integration with other components
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
(window as any).lastSeekedPosition = seekTime;
|
(window as any).lastSeekedPosition = seekTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,14 +202,14 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
const handleTouchEnd = () => {
|
const handleTouchEnd = () => {
|
||||||
setIsDraggingProgress(false);
|
setIsDraggingProgress(false);
|
||||||
isDraggingProgressRef.current = false;
|
isDraggingProgressRef.current = false;
|
||||||
document.removeEventListener('touchmove', handleTouchMove);
|
document.removeEventListener("touchmove", handleTouchMove);
|
||||||
document.removeEventListener('touchend', handleTouchEnd);
|
document.removeEventListener("touchend", handleTouchEnd);
|
||||||
document.removeEventListener('touchcancel', handleTouchEnd);
|
document.removeEventListener("touchcancel", handleTouchEnd);
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('touchmove', handleTouchMove, { passive: false });
|
document.addEventListener("touchmove", handleTouchMove, { passive: false });
|
||||||
document.addEventListener('touchend', handleTouchEnd);
|
document.addEventListener("touchend", handleTouchEnd);
|
||||||
document.addEventListener('touchcancel', handleTouchEnd);
|
document.addEventListener("touchcancel", handleTouchEnd);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle touch dragging on progress bar
|
// Handle touch dragging on progress bar
|
||||||
@@ -217,7 +217,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
if (!progressRef.current) return;
|
if (!progressRef.current) return;
|
||||||
|
|
||||||
// Get the touch coordinates
|
// Get the touch coordinates
|
||||||
const touch = 'touches' in e ? e.touches[0] : null;
|
const touch = "touches" in e ? e.touches[0] : null;
|
||||||
if (!touch) return;
|
if (!touch) return;
|
||||||
|
|
||||||
e.preventDefault(); // Prevent scrolling while dragging
|
e.preventDefault(); // Prevent scrolling while dragging
|
||||||
@@ -234,7 +234,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
setLastPosition(seekTime);
|
setLastPosition(seekTime);
|
||||||
|
|
||||||
// Also store globally for integration with other components
|
// Also store globally for integration with other components
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
(window as any).lastSeekedPosition = seekTime;
|
(window as any).lastSeekedPosition = seekTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
setLastPosition(seekTime);
|
setLastPosition(seekTime);
|
||||||
|
|
||||||
// Also store globally for integration with other components
|
// Also store globally for integration with other components
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
(window as any).lastSeekedPosition = seekTime;
|
(window as any).lastSeekedPosition = seekTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,24 +292,29 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (videoRef.current) {
|
if (videoRef.current) {
|
||||||
// Try to play with proper promise handling
|
// Try to play with proper promise handling
|
||||||
videoRef.current.play()
|
videoRef.current
|
||||||
|
.play()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.debug("iOS: Play started successfully at position:", videoRef.current?.currentTime);
|
logger.debug(
|
||||||
|
"iOS: Play started successfully at position:",
|
||||||
|
videoRef.current?.currentTime
|
||||||
|
);
|
||||||
onPlayPause(); // Update parent state after successful play
|
onPlayPause(); // Update parent state after successful play
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.error("iOS: Error playing video:", err);
|
console.error("iOS: Error playing video:", err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
} else {
|
} else {
|
||||||
// Normal play (non-iOS or no remembered position)
|
// Normal play (non-iOS or no remembered position)
|
||||||
video.play()
|
video
|
||||||
|
.play()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.debug("Normal: Play started successfully");
|
logger.debug("Normal: Play started successfully");
|
||||||
onPlayPause(); // Update parent state after successful play
|
onPlayPause(); // Update parent state after successful play
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.error("Error playing video:", err);
|
console.error("Error playing video:", err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -340,14 +345,12 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
{/* iOS First-play indicator - only shown on first visit for iOS devices when not initialized */}
|
{/* iOS First-play indicator - only shown on first visit for iOS devices when not initialized */}
|
||||||
{isIOS && !hasInitialized && !isPlaying && (
|
{isIOS && !hasInitialized && !isPlaying && (
|
||||||
<div className="ios-first-play-indicator">
|
<div className="ios-first-play-indicator">
|
||||||
<div className="ios-play-message">
|
<div className="ios-play-message">Tap Play to initialize video controls</div>
|
||||||
Tap Play to initialize video controls
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Play/Pause Indicator (shows based on current state) */}
|
{/* Play/Pause Indicator (shows based on current state) */}
|
||||||
<div className={`play-pause-indicator ${isPlaying ? 'pause-icon' : 'play-icon'}`}></div>
|
<div className={`play-pause-indicator ${isPlaying ? "pause-icon" : "play-icon"}`}></div>
|
||||||
|
|
||||||
{/* Video Controls Overlay */}
|
{/* Video Controls Overlay */}
|
||||||
<div className="video-controls">
|
<div className="video-controls">
|
||||||
@@ -360,26 +363,23 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
{/* Progress Bar with enhanced dragging */}
|
{/* Progress Bar with enhanced dragging */}
|
||||||
<div
|
<div
|
||||||
ref={progressRef}
|
ref={progressRef}
|
||||||
className={`video-progress ${isDraggingProgress ? 'dragging' : ''}`}
|
className={`video-progress ${isDraggingProgress ? "dragging" : ""}`}
|
||||||
onClick={handleProgressClick}
|
onClick={handleProgressClick}
|
||||||
onMouseDown={handleProgressDragStart}
|
onMouseDown={handleProgressDragStart}
|
||||||
onTouchStart={handleProgressTouchStart}
|
onTouchStart={handleProgressTouchStart}
|
||||||
>
|
>
|
||||||
<div
|
<div className="video-progress-fill" style={{ width: `${progressPercentage}%` }}></div>
|
||||||
className="video-progress-fill"
|
<div className="video-scrubber" style={{ left: `${progressPercentage}%` }}></div>
|
||||||
style={{ width: `${progressPercentage}%` }}
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
className="video-scrubber"
|
|
||||||
style={{ left: `${progressPercentage}%` }}
|
|
||||||
></div>
|
|
||||||
|
|
||||||
{/* Floating time tooltip when dragging */}
|
{/* Floating time tooltip when dragging */}
|
||||||
{isDraggingProgress && (
|
{isDraggingProgress && (
|
||||||
<div className="video-time-tooltip" style={{
|
<div
|
||||||
|
className="video-time-tooltip"
|
||||||
|
style={{
|
||||||
left: `${tooltipPosition.x}px`,
|
left: `${tooltipPosition.x}px`,
|
||||||
transform: 'translateX(-50%)'
|
transform: "translateX(-50%)"
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{formatDetailedTime(tooltipTime)}
|
{formatDetailedTime(tooltipTime)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -396,7 +396,15 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
data-tooltip={isMuted ? "Unmute" : "Mute"}
|
data-tooltip={isMuted ? "Unmute" : "Mute"}
|
||||||
>
|
>
|
||||||
{isMuted ? (
|
{isMuted ? (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
<line x1="1" y1="1" x2="23" y2="23"></line>
|
<line x1="1" y1="1" x2="23" y2="23"></line>
|
||||||
<path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"></path>
|
<path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"></path>
|
||||||
<path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"></path>
|
<path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"></path>
|
||||||
@@ -404,7 +412,15 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
<line x1="8" y1="23" x2="16" y2="23"></line>
|
<line x1="8" y1="23" x2="16" y2="23"></line>
|
||||||
</svg>
|
</svg>
|
||||||
) : (
|
) : (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
||||||
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
|
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -420,7 +436,11 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
data-tooltip="Toggle fullscreen"
|
data-tooltip="Toggle fullscreen"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path fillRule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 01-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 011.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 011.414-1.414L15 13.586V12a1 1 0 011-1z" clipRule="evenodd" />
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 01-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 011.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 011.414-1.414L15 13.586V12a1 1 0 011-1z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ const useVideoTrimmer = () => {
|
|||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [isMuted, setIsMuted] = useState(false);
|
const [isMuted, setIsMuted] = useState(false);
|
||||||
|
|
||||||
// Preview mode state for playing only segments
|
|
||||||
const [isPreviewMode, setIsPreviewMode] = useState(false);
|
|
||||||
const [previewSegmentIndex, setPreviewSegmentIndex] = useState(0);
|
|
||||||
|
|
||||||
// Timeline state
|
// Timeline state
|
||||||
const [thumbnails, setThumbnails] = useState<string[]>([]);
|
const [thumbnails, setThumbnails] = useState<string[]>([]);
|
||||||
const [trimStart, setTrimStart] = useState(0);
|
const [trimStart, setTrimStart] = useState(0);
|
||||||
@@ -50,18 +46,21 @@ const useVideoTrimmer = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (history.length > 0) {
|
if (history.length > 0) {
|
||||||
// For debugging - moved to console.debug
|
// For debugging - moved to console.debug
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === "development") {
|
||||||
console.debug(`History state updated: ${history.length} entries, position: ${historyPosition}`);
|
console.debug(
|
||||||
// Log actions in history to help debug undo/redo
|
`History state updated: ${history.length} entries, position: ${historyPosition}`
|
||||||
const actions = history.map((state, idx) =>
|
|
||||||
`${idx}: ${state.action || 'unknown'} (segments: ${state.clipSegments.length})`
|
|
||||||
);
|
);
|
||||||
console.debug('History actions:', actions);
|
// Log actions in history to help debug undo/redo
|
||||||
|
const actions = history.map(
|
||||||
|
(state, idx) =>
|
||||||
|
`${idx}: ${state.action || "unknown"} (segments: ${state.clipSegments.length})`
|
||||||
|
);
|
||||||
|
console.debug("History actions:", actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's at least one history entry and it wasn't a save operation, mark as having unsaved changes
|
// If there's at least one history entry and it wasn't a save operation, mark as having unsaved changes
|
||||||
const lastAction = history[historyPosition]?.action || '';
|
const lastAction = history[historyPosition]?.action || "";
|
||||||
if (lastAction !== 'save' && lastAction !== 'save_copy' && lastAction !== 'save_segments') {
|
if (lastAction !== "save" && lastAction !== "save_copy" && lastAction !== "save_segments") {
|
||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +72,7 @@ const useVideoTrimmer = () => {
|
|||||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||||
if (hasUnsavedChanges) {
|
if (hasUnsavedChanges) {
|
||||||
// Standard way of showing a confirmation dialog before leaving
|
// Standard way of showing a confirmation dialog before leaving
|
||||||
const message = 'Your edits will get lost if you leave the page. Do you want to continue?';
|
const message = "Your edits will get lost if you leave the page. Do you want to continue?";
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.returnValue = message; // Chrome requires returnValue to be set
|
e.returnValue = message; // Chrome requires returnValue to be set
|
||||||
return message; // For other browsers
|
return message; // For other browsers
|
||||||
@@ -81,11 +80,11 @@ const useVideoTrimmer = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add event listener
|
// Add event listener
|
||||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||||
};
|
};
|
||||||
}, [hasUnsavedChanges]);
|
}, [hasUnsavedChanges]);
|
||||||
|
|
||||||
@@ -146,18 +145,12 @@ const useVideoTrimmer = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePlay = () => {
|
const handlePlay = () => {
|
||||||
// Only update isPlaying if we're not in preview mode
|
|
||||||
if (!isPreviewMode) {
|
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
setVideoInitialized(true);
|
setVideoInitialized(true);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePause = () => {
|
const handlePause = () => {
|
||||||
// Only update isPlaying if we're not in preview mode
|
|
||||||
if (!isPreviewMode) {
|
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEnded = () => {
|
const handleEnded = () => {
|
||||||
@@ -166,21 +159,21 @@ const useVideoTrimmer = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add event listeners
|
// Add event listeners
|
||||||
video.addEventListener('loadedmetadata', handleLoadedMetadata);
|
video.addEventListener("loadedmetadata", handleLoadedMetadata);
|
||||||
video.addEventListener('timeupdate', handleTimeUpdate);
|
video.addEventListener("timeupdate", handleTimeUpdate);
|
||||||
video.addEventListener('play', handlePlay);
|
video.addEventListener("play", handlePlay);
|
||||||
video.addEventListener('pause', handlePause);
|
video.addEventListener("pause", handlePause);
|
||||||
video.addEventListener('ended', handleEnded);
|
video.addEventListener("ended", handleEnded);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Remove event listeners
|
// Remove event listeners
|
||||||
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
video.removeEventListener("loadedmetadata", handleLoadedMetadata);
|
||||||
video.removeEventListener('timeupdate', handleTimeUpdate);
|
video.removeEventListener("timeupdate", handleTimeUpdate);
|
||||||
video.removeEventListener('play', handlePlay);
|
video.removeEventListener("play", handlePlay);
|
||||||
video.removeEventListener('pause', handlePause);
|
video.removeEventListener("pause", handlePause);
|
||||||
video.removeEventListener('ended', handleEnded);
|
video.removeEventListener("ended", handleEnded);
|
||||||
};
|
};
|
||||||
}, [isPreviewMode]);
|
}, []);
|
||||||
|
|
||||||
// Play/pause video
|
// Play/pause video
|
||||||
const playPauseVideo = () => {
|
const playPauseVideo = () => {
|
||||||
@@ -191,7 +184,7 @@ const useVideoTrimmer = () => {
|
|||||||
video.pause();
|
video.pause();
|
||||||
} else {
|
} else {
|
||||||
// iOS Safari fix: Use the last seeked position if available
|
// iOS Safari fix: Use the last seeked position if available
|
||||||
if (!isPlaying && typeof window !== 'undefined' && window.lastSeekedPosition > 0) {
|
if (!isPlaying && typeof window !== "undefined" && window.lastSeekedPosition > 0) {
|
||||||
// Only apply this if the video is not at the same position already
|
// Only apply this if the video is not at the same position already
|
||||||
// This avoids unnecessary seeking which might cause playback issues
|
// This avoids unnecessary seeking which might cause playback issues
|
||||||
if (Math.abs(video.currentTime - window.lastSeekedPosition) > 0.1) {
|
if (Math.abs(video.currentTime - window.lastSeekedPosition) > 0.1) {
|
||||||
@@ -203,15 +196,16 @@ const useVideoTrimmer = () => {
|
|||||||
video.currentTime = trimStart;
|
video.currentTime = trimStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
video.play()
|
video
|
||||||
|
.play()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Play started successfully
|
// Play started successfully
|
||||||
// Reset the last seeked position after successfully starting playback
|
// Reset the last seeked position after successfully starting playback
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
window.lastSeekedPosition = 0;
|
window.lastSeekedPosition = 0;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.error("Error starting playback:", err);
|
console.error("Error starting playback:", err);
|
||||||
setIsPlaying(false); // Reset state if play failed
|
setIsPlaying(false); // Reset state if play failed
|
||||||
});
|
});
|
||||||
@@ -226,51 +220,25 @@ const useVideoTrimmer = () => {
|
|||||||
// Track if the video was playing before seeking
|
// Track if the video was playing before seeking
|
||||||
const wasPlaying = !video.paused;
|
const wasPlaying = !video.paused;
|
||||||
|
|
||||||
// Store current preview mode state to preserve it
|
|
||||||
const wasInPreviewMode = isPreviewMode;
|
|
||||||
|
|
||||||
// Update the video position
|
// Update the video position
|
||||||
video.currentTime = time;
|
video.currentTime = time;
|
||||||
setCurrentTime(time);
|
setCurrentTime(time);
|
||||||
|
|
||||||
// Store the position in a global state accessible to iOS Safari
|
// Store the position in a global state accessible to iOS Safari
|
||||||
// This ensures when play is pressed later, it remembers the position
|
// This ensures when play is pressed later, it remembers the position
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
window.lastSeekedPosition = time;
|
window.lastSeekedPosition = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find segment at this position for preview mode playback
|
// Resume playback if it was playing before
|
||||||
if (wasInPreviewMode) {
|
if (wasPlaying) {
|
||||||
const segmentAtPosition = clipSegments.find(
|
|
||||||
seg => time >= seg.startTime && time <= seg.endTime
|
|
||||||
);
|
|
||||||
|
|
||||||
if (segmentAtPosition) {
|
|
||||||
// Update the active segment index in preview mode
|
|
||||||
const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
|
||||||
const newSegmentIndex = orderedSegments.findIndex(seg => seg.id === segmentAtPosition.id);
|
|
||||||
if (newSegmentIndex !== -1) {
|
|
||||||
setPreviewSegmentIndex(newSegmentIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resume playback in two scenarios:
|
|
||||||
// 1. If it was playing before (regular mode)
|
|
||||||
// 2. If we're in preview mode (regardless of previous state)
|
|
||||||
if (wasPlaying || wasInPreviewMode) {
|
|
||||||
// Ensure preview mode stays on if it was on before
|
|
||||||
if (wasInPreviewMode) {
|
|
||||||
setIsPreviewMode(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Play immediately without delay
|
// Play immediately without delay
|
||||||
video.play()
|
video
|
||||||
|
.play()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setIsPlaying(true); // Update state to reflect we're playing
|
setIsPlaying(true); // Update state to reflect we're playing
|
||||||
// "Resumed playback after seeking in " + (wasInPreviewMode ? "preview" : "regular") + " mode"
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.error("Error resuming playback:", err);
|
console.error("Error resuming playback:", err);
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
});
|
});
|
||||||
@@ -286,7 +254,7 @@ const useVideoTrimmer = () => {
|
|||||||
trimEnd,
|
trimEnd,
|
||||||
splitPoints: [...splitPoints],
|
splitPoints: [...splitPoints],
|
||||||
clipSegments: JSON.parse(JSON.stringify(clipSegments)), // Deep clone to avoid reference issues
|
clipSegments: JSON.parse(JSON.stringify(clipSegments)), // Deep clone to avoid reference issues
|
||||||
action: action || 'manual_save' // Track the action that triggered this save
|
action: action || "manual_save" // Track the action that triggered this save
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if state is significantly different from last saved state
|
// Check if state is significantly different from last saved state
|
||||||
@@ -306,8 +274,10 @@ const useVideoTrimmer = () => {
|
|||||||
if (!oldSeg || !newSeg) return true;
|
if (!oldSeg || !newSeg) return true;
|
||||||
|
|
||||||
// Check if any time values changed by more than 0.001 seconds (1ms)
|
// Check if any time values changed by more than 0.001 seconds (1ms)
|
||||||
if (Math.abs(oldSeg.startTime - newSeg.startTime) > 0.001 ||
|
if (
|
||||||
Math.abs(oldSeg.endTime - newSeg.endTime) > 0.001) {
|
Math.abs(oldSeg.startTime - newSeg.startTime) > 0.001 ||
|
||||||
|
Math.abs(oldSeg.endTime - newSeg.endTime) > 0.001
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,7 +285,8 @@ const useVideoTrimmer = () => {
|
|||||||
return false; // No significant changes found
|
return false; // No significant changes found
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSignificantChange = !lastState ||
|
const isSignificantChange =
|
||||||
|
!lastState ||
|
||||||
lastState.trimStart !== newState.trimStart ||
|
lastState.trimStart !== newState.trimStart ||
|
||||||
lastState.trimEnd !== newState.trimEnd ||
|
lastState.trimEnd !== newState.trimEnd ||
|
||||||
lastState.splitPoints.length !== newState.splitPoints.length ||
|
lastState.splitPoints.length !== newState.splitPoints.length ||
|
||||||
@@ -330,7 +301,7 @@ const useVideoTrimmer = () => {
|
|||||||
const currentPosition = historyPosition;
|
const currentPosition = historyPosition;
|
||||||
|
|
||||||
// Use functional updates to ensure we're working with the latest state
|
// Use functional updates to ensure we're working with the latest state
|
||||||
setHistory(prevHistory => {
|
setHistory((prevHistory) => {
|
||||||
// If we're not at the end of history, truncate
|
// If we're not at the end of history, truncate
|
||||||
if (currentPosition < prevHistory.length - 1) {
|
if (currentPosition < prevHistory.length - 1) {
|
||||||
const newHistory = prevHistory.slice(0, currentPosition + 1);
|
const newHistory = prevHistory.slice(0, currentPosition + 1);
|
||||||
@@ -342,7 +313,7 @@ const useVideoTrimmer = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update position using functional update
|
// Update position using functional update
|
||||||
setHistoryPosition(prev => {
|
setHistoryPosition((prev) => {
|
||||||
const newPosition = prev + 1;
|
const newPosition = prev + 1;
|
||||||
// "Saved state to history position", newPosition)
|
// "Saved state to history position", newPosition)
|
||||||
return newPosition;
|
return newPosition;
|
||||||
@@ -368,16 +339,16 @@ const useVideoTrimmer = () => {
|
|||||||
if (recordHistory) {
|
if (recordHistory) {
|
||||||
// Use a small timeout to ensure the state is updated
|
// Use a small timeout to ensure the state is updated
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
saveState(action || (isStart ? 'adjust_trim_start' : 'adjust_trim_end'));
|
saveState(action || (isStart ? "adjust_trim_start" : "adjust_trim_end"));
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('update-trim', handleTrimUpdate as EventListener);
|
document.addEventListener("update-trim", handleTrimUpdate as EventListener);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('update-trim', handleTrimUpdate as EventListener);
|
document.removeEventListener("update-trim", handleTrimUpdate as EventListener);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -389,10 +360,12 @@ const useVideoTrimmer = () => {
|
|||||||
// Default to true to ensure all segment changes are recorded
|
// Default to true to ensure all segment changes are recorded
|
||||||
const isSignificantChange = e.detail.recordHistory !== false;
|
const isSignificantChange = e.detail.recordHistory !== false;
|
||||||
// Get the action type if provided
|
// Get the action type if provided
|
||||||
const actionType = e.detail.action || 'update_segments';
|
const actionType = e.detail.action || "update_segments";
|
||||||
|
|
||||||
// Log the update details
|
// Log the update details
|
||||||
logger.debug(`Updating segments with action: ${actionType}, recordHistory: ${isSignificantChange ? "true" : "false"}`);
|
logger.debug(
|
||||||
|
`Updating segments with action: ${actionType}, recordHistory: ${isSignificantChange ? "true" : "false"}`
|
||||||
|
);
|
||||||
|
|
||||||
// Update segment state immediately for UI feedback
|
// Update segment state immediately for UI feedback
|
||||||
setClipSegments(e.detail.segments);
|
setClipSegments(e.detail.segments);
|
||||||
@@ -418,7 +391,7 @@ const useVideoTrimmer = () => {
|
|||||||
const currentHistoryPosition = historyPosition;
|
const currentHistoryPosition = historyPosition;
|
||||||
|
|
||||||
// Update history with the functional pattern to avoid stale closure issues
|
// Update history with the functional pattern to avoid stale closure issues
|
||||||
setHistory(prevHistory => {
|
setHistory((prevHistory) => {
|
||||||
// If we're not at the end of the history, truncate
|
// If we're not at the end of the history, truncate
|
||||||
if (currentHistoryPosition < prevHistory.length - 1) {
|
if (currentHistoryPosition < prevHistory.length - 1) {
|
||||||
const newHistory = prevHistory.slice(0, currentHistoryPosition + 1);
|
const newHistory = prevHistory.slice(0, currentHistoryPosition + 1);
|
||||||
@@ -430,24 +403,29 @@ const useVideoTrimmer = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Ensure the historyPosition is updated to the correct position
|
// Ensure the historyPosition is updated to the correct position
|
||||||
setHistoryPosition(prev => {
|
setHistoryPosition((prev) => {
|
||||||
const newPosition = prev + 1;
|
const newPosition = prev + 1;
|
||||||
logger.debug(`Saved state with action: ${actionType} to history position ${newPosition}`);
|
logger.debug(
|
||||||
|
`Saved state with action: ${actionType} to history position ${newPosition}`
|
||||||
|
);
|
||||||
return newPosition;
|
return newPosition;
|
||||||
});
|
});
|
||||||
}, 20); // Slightly increased delay to ensure state updates are complete
|
}, 20); // Slightly increased delay to ensure state updates are complete
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Skipped saving state to history for action: ${actionType} (recordHistory=false)`);
|
logger.debug(
|
||||||
|
`Skipped saving state to history for action: ${actionType} (recordHistory=false)`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSplitSegment = async (e: Event) => {
|
const handleSplitSegment = async (e: Event) => {
|
||||||
const customEvent = e as CustomEvent;
|
const customEvent = e as CustomEvent;
|
||||||
if (customEvent.detail &&
|
if (
|
||||||
typeof customEvent.detail.time === 'number' &&
|
customEvent.detail &&
|
||||||
typeof customEvent.detail.segmentId === 'number') {
|
typeof customEvent.detail.time === "number" &&
|
||||||
|
typeof customEvent.detail.segmentId === "number"
|
||||||
|
) {
|
||||||
// Get the time and segment ID from the event
|
// Get the time and segment ID from the event
|
||||||
const timeToSplit = customEvent.detail.time;
|
const timeToSplit = customEvent.detail.time;
|
||||||
const segmentId = customEvent.detail.segmentId;
|
const segmentId = customEvent.detail.segmentId;
|
||||||
@@ -456,7 +434,7 @@ const useVideoTrimmer = () => {
|
|||||||
seekVideo(timeToSplit);
|
seekVideo(timeToSplit);
|
||||||
|
|
||||||
// Find the segment to split
|
// Find the segment to split
|
||||||
const segmentToSplit = clipSegments.find(seg => seg.id === segmentId);
|
const segmentToSplit = clipSegments.find((seg) => seg.id === segmentId);
|
||||||
if (!segmentToSplit) return;
|
if (!segmentToSplit) return;
|
||||||
|
|
||||||
// Make sure the split point is within the segment
|
// Make sure the split point is within the segment
|
||||||
@@ -468,7 +446,7 @@ const useVideoTrimmer = () => {
|
|||||||
const newSegments = [...clipSegments];
|
const newSegments = [...clipSegments];
|
||||||
|
|
||||||
// Remove the original segment
|
// Remove the original segment
|
||||||
const segmentIndex = newSegments.findIndex(seg => seg.id === segmentId);
|
const segmentIndex = newSegments.findIndex((seg) => seg.id === segmentId);
|
||||||
if (segmentIndex === -1) return;
|
if (segmentIndex === -1) return;
|
||||||
|
|
||||||
newSegments.splice(segmentIndex, 1);
|
newSegments.splice(segmentIndex, 1);
|
||||||
@@ -479,7 +457,7 @@ const useVideoTrimmer = () => {
|
|||||||
name: `${segmentToSplit.name}-A`,
|
name: `${segmentToSplit.name}-A`,
|
||||||
startTime: segmentToSplit.startTime,
|
startTime: segmentToSplit.startTime,
|
||||||
endTime: timeToSplit,
|
endTime: timeToSplit,
|
||||||
thumbnail: '' // Empty placeholder - we'll use dynamic colors instead
|
thumbnail: "" // Empty placeholder - we'll use dynamic colors instead
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create second half of the split segment - no thumbnail needed
|
// Create second half of the split segment - no thumbnail needed
|
||||||
@@ -488,7 +466,7 @@ const useVideoTrimmer = () => {
|
|||||||
name: `${segmentToSplit.name}-B`,
|
name: `${segmentToSplit.name}-B`,
|
||||||
startTime: timeToSplit,
|
startTime: timeToSplit,
|
||||||
endTime: segmentToSplit.endTime,
|
endTime: segmentToSplit.endTime,
|
||||||
thumbnail: '' // Empty placeholder - we'll use dynamic colors instead
|
thumbnail: "" // Empty placeholder - we'll use dynamic colors instead
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the new segments
|
// Add the new segments
|
||||||
@@ -499,18 +477,18 @@ const useVideoTrimmer = () => {
|
|||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
setClipSegments(newSegments);
|
setClipSegments(newSegments);
|
||||||
saveState('split_segment');
|
saveState("split_segment");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle delete segment event
|
// Handle delete segment event
|
||||||
const handleDeleteSegment = async (e: Event) => {
|
const handleDeleteSegment = async (e: Event) => {
|
||||||
const customEvent = e as CustomEvent;
|
const customEvent = e as CustomEvent;
|
||||||
if (customEvent.detail && typeof customEvent.detail.segmentId === 'number') {
|
if (customEvent.detail && typeof customEvent.detail.segmentId === "number") {
|
||||||
const segmentId = customEvent.detail.segmentId;
|
const segmentId = customEvent.detail.segmentId;
|
||||||
|
|
||||||
// Find and remove the segment
|
// Find and remove the segment
|
||||||
const newSegments = clipSegments.filter(segment => segment.id !== segmentId);
|
const newSegments = clipSegments.filter((segment) => segment.id !== segmentId);
|
||||||
|
|
||||||
if (newSegments.length !== clipSegments.length) {
|
if (newSegments.length !== clipSegments.length) {
|
||||||
// If all segments are deleted, create a new full video segment
|
// If all segments are deleted, create a new full video segment
|
||||||
@@ -522,7 +500,7 @@ const useVideoTrimmer = () => {
|
|||||||
name: "segment",
|
name: "segment",
|
||||||
startTime: 0,
|
startTime: 0,
|
||||||
endTime: videoRef.current.duration,
|
endTime: videoRef.current.duration,
|
||||||
thumbnail: '' // Empty placeholder - we'll use dynamic colors instead
|
thumbnail: "" // Empty placeholder - we'll use dynamic colors instead
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset the trim points as well
|
// Reset the trim points as well
|
||||||
@@ -534,179 +512,32 @@ const useVideoTrimmer = () => {
|
|||||||
// Just update the segments normally
|
// Just update the segments normally
|
||||||
setClipSegments(newSegments);
|
setClipSegments(newSegments);
|
||||||
}
|
}
|
||||||
saveState('delete_segment');
|
saveState("delete_segment");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('update-segments', handleUpdateSegments as EventListener);
|
document.addEventListener("update-segments", handleUpdateSegments as EventListener);
|
||||||
document.addEventListener('split-segment', handleSplitSegment as EventListener);
|
document.addEventListener("split-segment", handleSplitSegment as EventListener);
|
||||||
document.addEventListener('delete-segment', handleDeleteSegment as EventListener);
|
document.addEventListener("delete-segment", handleDeleteSegment as EventListener);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('update-segments', handleUpdateSegments as EventListener);
|
document.removeEventListener("update-segments", handleUpdateSegments as EventListener);
|
||||||
document.removeEventListener('split-segment', handleSplitSegment as EventListener);
|
document.removeEventListener("split-segment", handleSplitSegment as EventListener);
|
||||||
document.removeEventListener('delete-segment', handleDeleteSegment as EventListener);
|
document.removeEventListener("delete-segment", handleDeleteSegment as EventListener);
|
||||||
};
|
};
|
||||||
}, [clipSegments, duration]);
|
}, [clipSegments, duration]);
|
||||||
|
|
||||||
// Preview mode effect to handle playing only segments
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isPreviewMode || !videoRef.current) return;
|
|
||||||
|
|
||||||
// Sort segments by start time
|
|
||||||
const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
|
||||||
if (orderedSegments.length === 0) return;
|
|
||||||
|
|
||||||
const video = videoRef.current;
|
|
||||||
|
|
||||||
// Function to handle segment playback
|
|
||||||
const handleSegmentPlayback = () => {
|
|
||||||
if (!isPreviewMode || !video) return;
|
|
||||||
|
|
||||||
const currentSegment = orderedSegments[previewSegmentIndex];
|
|
||||||
if (!currentSegment) return;
|
|
||||||
|
|
||||||
const currentTime = video.currentTime;
|
|
||||||
|
|
||||||
// If we're before the current segment's start, jump to it
|
|
||||||
if (currentTime < currentSegment.startTime) {
|
|
||||||
video.currentTime = currentSegment.startTime;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've reached the end of the current segment
|
|
||||||
if (currentTime >= currentSegment.endTime - 0.01) { // Small threshold to ensure smooth transition
|
|
||||||
// Move to the next segment if available
|
|
||||||
if (previewSegmentIndex < orderedSegments.length - 1) {
|
|
||||||
// Play next segment
|
|
||||||
const nextSegment = orderedSegments[previewSegmentIndex + 1];
|
|
||||||
video.currentTime = nextSegment.startTime;
|
|
||||||
setPreviewSegmentIndex(previewSegmentIndex + 1);
|
|
||||||
|
|
||||||
logger.debug("Preview: Moving to next segment", {
|
|
||||||
from: formatDetailedTime(currentSegment.endTime),
|
|
||||||
to: formatDetailedTime(nextSegment.startTime),
|
|
||||||
segmentIndex: previewSegmentIndex + 1
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Loop back to first segment
|
|
||||||
logger.debug("Preview: Looping back to first segment");
|
|
||||||
video.currentTime = orderedSegments[0].startTime;
|
|
||||||
setPreviewSegmentIndex(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure playback continues
|
|
||||||
video.play().catch(err => {
|
|
||||||
console.error("Error continuing preview playback:", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add event listener for timeupdate to check segment boundaries
|
|
||||||
video.addEventListener('timeupdate', handleSegmentPlayback);
|
|
||||||
|
|
||||||
// Start playing if not already playing
|
|
||||||
if (video.paused) {
|
|
||||||
video.currentTime = orderedSegments[previewSegmentIndex].startTime;
|
|
||||||
video.play().catch(err => {
|
|
||||||
console.error("Error starting preview playback:", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (video) {
|
|
||||||
video.removeEventListener('timeupdate', handleSegmentPlayback);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [isPreviewMode, previewSegmentIndex, clipSegments]);
|
|
||||||
|
|
||||||
// Handle starting preview mode
|
|
||||||
const handleStartPreview = () => {
|
|
||||||
const video = videoRef.current;
|
|
||||||
if (!video || clipSegments.length === 0) return;
|
|
||||||
|
|
||||||
// If preview is already active, do nothing
|
|
||||||
if (isPreviewMode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If normal playback is happening, pause it
|
|
||||||
if (isPlaying) {
|
|
||||||
video.pause();
|
|
||||||
setIsPlaying(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort segments by start time
|
|
||||||
const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
|
||||||
if (orderedSegments.length === 0) return;
|
|
||||||
|
|
||||||
// Set the preview mode flag
|
|
||||||
setIsPreviewMode(true);
|
|
||||||
logger.debug("Entering preview mode");
|
|
||||||
|
|
||||||
// Set the first segment as the current one in the preview sequence
|
|
||||||
setPreviewSegmentIndex(0);
|
|
||||||
|
|
||||||
// Move to the start of the first segment
|
|
||||||
video.currentTime = orderedSegments[0].startTime;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle playing/stopping preview mode
|
|
||||||
const handlePreview = () => {
|
|
||||||
const video = videoRef.current;
|
|
||||||
if (!video || clipSegments.length === 0) return;
|
|
||||||
|
|
||||||
// If preview is already active, turn it off
|
|
||||||
if (isPreviewMode) {
|
|
||||||
setIsPreviewMode(false);
|
|
||||||
|
|
||||||
// Always pause the video when exiting preview mode
|
|
||||||
video.pause();
|
|
||||||
setIsPlaying(false);
|
|
||||||
|
|
||||||
logger.debug("Exiting preview mode - video paused");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort segments by start time
|
|
||||||
const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
|
||||||
if (orderedSegments.length === 0) return;
|
|
||||||
|
|
||||||
// Set the preview mode flag
|
|
||||||
setIsPreviewMode(true);
|
|
||||||
logger.debug("Entering preview mode");
|
|
||||||
|
|
||||||
// Set the first segment as the current one in the preview sequence
|
|
||||||
setPreviewSegmentIndex(0);
|
|
||||||
|
|
||||||
// Start preview mode by playing the first segment
|
|
||||||
video.currentTime = orderedSegments[0].startTime;
|
|
||||||
|
|
||||||
// Start playback
|
|
||||||
video.play()
|
|
||||||
.then(() => {
|
|
||||||
setIsPlaying(true);
|
|
||||||
logger.debug("Preview started successfully");
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error("Error starting preview:", err);
|
|
||||||
setIsPreviewMode(false);
|
|
||||||
setIsPlaying(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle trim start change
|
// Handle trim start change
|
||||||
const handleTrimStartChange = (time: number) => {
|
const handleTrimStartChange = (time: number) => {
|
||||||
setTrimStart(time);
|
setTrimStart(time);
|
||||||
saveState('adjust_trim_start');
|
saveState("adjust_trim_start");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle trim end change
|
// Handle trim end change
|
||||||
const handleTrimEndChange = (time: number) => {
|
const handleTrimEndChange = (time: number) => {
|
||||||
setTrimEnd(time);
|
setTrimEnd(time);
|
||||||
saveState('adjust_trim_end');
|
saveState("adjust_trim_end");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle split at current position
|
// Handle split at current position
|
||||||
@@ -732,7 +563,7 @@ const useVideoTrimmer = () => {
|
|||||||
name: `Segment ${i + 1}`,
|
name: `Segment ${i + 1}`,
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
thumbnail: '' // Empty placeholder - we'll use dynamic colors instead
|
thumbnail: "" // Empty placeholder - we'll use dynamic colors instead
|
||||||
});
|
});
|
||||||
|
|
||||||
startTime = endTime;
|
startTime = endTime;
|
||||||
@@ -740,7 +571,7 @@ const useVideoTrimmer = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setClipSegments(newSegments);
|
setClipSegments(newSegments);
|
||||||
saveState('create_split_points');
|
saveState("create_split_points");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -759,23 +590,29 @@ const useVideoTrimmer = () => {
|
|||||||
name: "segment",
|
name: "segment",
|
||||||
startTime: 0,
|
startTime: 0,
|
||||||
endTime: duration,
|
endTime: duration,
|
||||||
thumbnail: '' // Empty placeholder - we'll use dynamic colors instead
|
thumbnail: "" // Empty placeholder - we'll use dynamic colors instead
|
||||||
};
|
};
|
||||||
|
|
||||||
setClipSegments([defaultSegment]);
|
setClipSegments([defaultSegment]);
|
||||||
saveState('reset_all');
|
saveState("reset_all");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle undo
|
// Handle undo
|
||||||
const handleUndo = () => {
|
const handleUndo = () => {
|
||||||
if (historyPosition > 0) {
|
if (historyPosition > 0) {
|
||||||
const previousState = history[historyPosition - 1];
|
const previousState = history[historyPosition - 1];
|
||||||
logger.debug(`** UNDO ** to position ${historyPosition - 1}, action: ${previousState.action}, segments: ${previousState.clipSegments.length}`);
|
logger.debug(
|
||||||
|
`** UNDO ** to position ${historyPosition - 1}, action: ${previousState.action}, segments: ${previousState.clipSegments.length}`
|
||||||
|
);
|
||||||
|
|
||||||
// Log segment details to help debug
|
// Log segment details to help debug
|
||||||
logger.debug("Segment details after undo:", previousState.clipSegments.map(seg =>
|
logger.debug(
|
||||||
|
"Segment details after undo:",
|
||||||
|
previousState.clipSegments.map(
|
||||||
|
(seg) =>
|
||||||
`ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}`
|
`ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}`
|
||||||
));
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Apply the previous state with deep cloning to avoid reference issues
|
// Apply the previous state with deep cloning to avoid reference issues
|
||||||
setTrimStart(previousState.trimStart);
|
setTrimStart(previousState.trimStart);
|
||||||
@@ -792,12 +629,18 @@ const useVideoTrimmer = () => {
|
|||||||
const handleRedo = () => {
|
const handleRedo = () => {
|
||||||
if (historyPosition < history.length - 1) {
|
if (historyPosition < history.length - 1) {
|
||||||
const nextState = history[historyPosition + 1];
|
const nextState = history[historyPosition + 1];
|
||||||
logger.debug(`** REDO ** to position ${historyPosition + 1}, action: ${nextState.action}, segments: ${nextState.clipSegments.length}`);
|
logger.debug(
|
||||||
|
`** REDO ** to position ${historyPosition + 1}, action: ${nextState.action}, segments: ${nextState.clipSegments.length}`
|
||||||
|
);
|
||||||
|
|
||||||
// Log segment details to help debug
|
// Log segment details to help debug
|
||||||
logger.debug("Segment details after redo:", nextState.clipSegments.map(seg =>
|
logger.debug(
|
||||||
|
"Segment details after redo:",
|
||||||
|
nextState.clipSegments.map(
|
||||||
|
(seg) =>
|
||||||
`ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}`
|
`ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}`
|
||||||
));
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Apply the next state with deep cloning to avoid reference issues
|
// Apply the next state with deep cloning to avoid reference issues
|
||||||
setTrimStart(nextState.trimStart);
|
setTrimStart(nextState.trimStart);
|
||||||
@@ -820,23 +663,13 @@ const useVideoTrimmer = () => {
|
|||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
if (!video) return;
|
if (!video) return;
|
||||||
|
|
||||||
// If in preview mode, exit it before toggling normal play
|
|
||||||
if (isPreviewMode) {
|
|
||||||
setIsPreviewMode(false);
|
|
||||||
// Don't immediately start playing when exiting preview mode
|
|
||||||
// Just update the state and return
|
|
||||||
setIsPlaying(false);
|
|
||||||
video.pause();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
// Pause the video
|
// Pause the video
|
||||||
video.pause();
|
video.pause();
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
} else {
|
} else {
|
||||||
// iOS Safari fix: Check for lastSeekedPosition
|
// iOS Safari fix: Check for lastSeekedPosition
|
||||||
if (typeof window !== 'undefined' && window.lastSeekedPosition > 0) {
|
if (typeof window !== "undefined" && window.lastSeekedPosition > 0) {
|
||||||
// Only seek if the position is significantly different
|
// Only seek if the position is significantly different
|
||||||
if (Math.abs(video.currentTime - window.lastSeekedPosition) > 0.1) {
|
if (Math.abs(video.currentTime - window.lastSeekedPosition) > 0.1) {
|
||||||
console.log("handlePlay: Using lastSeekedPosition", window.lastSeekedPosition);
|
console.log("handlePlay: Using lastSeekedPosition", window.lastSeekedPosition);
|
||||||
@@ -845,15 +678,16 @@ const useVideoTrimmer = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Play the video from current position with proper promise handling
|
// Play the video from current position with proper promise handling
|
||||||
video.play()
|
video
|
||||||
|
.play()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
// Reset lastSeekedPosition after successful play
|
// Reset lastSeekedPosition after successful play
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
window.lastSeekedPosition = 0;
|
window.lastSeekedPosition = 0;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.error("Error playing video:", err);
|
console.error("Error playing video:", err);
|
||||||
setIsPlaying(false); // Reset state if play failed
|
setIsPlaying(false); // Reset state if play failed
|
||||||
});
|
});
|
||||||
@@ -877,14 +711,14 @@ const useVideoTrimmer = () => {
|
|||||||
// Create the JSON data for saving
|
// Create the JSON data for saving
|
||||||
const saveData = {
|
const saveData = {
|
||||||
type: "save",
|
type: "save",
|
||||||
segments: sortedSegments.map(segment => ({
|
segments: sortedSegments.map((segment) => ({
|
||||||
startTime: formatDetailedTime(segment.startTime),
|
startTime: formatDetailedTime(segment.startTime),
|
||||||
endTime: formatDetailedTime(segment.endTime)
|
endTime: formatDetailedTime(segment.endTime)
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
// Display JSON in alert (for demonstration purposes)
|
// Display JSON in alert (for demonstration purposes)
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === "development") {
|
||||||
console.debug("Saving data:", saveData);
|
console.debug("Saving data:", saveData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -892,12 +726,12 @@ const useVideoTrimmer = () => {
|
|||||||
setHasUnsavedChanges(false);
|
setHasUnsavedChanges(false);
|
||||||
|
|
||||||
// Debug message
|
// Debug message
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === "development") {
|
||||||
console.debug("Changes saved - reset unsaved changes flag");
|
console.debug("Changes saved - reset unsaved changes flag");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to history with special "save" action to mark saved state
|
// Save to history with special "save" action to mark saved state
|
||||||
saveState('save');
|
saveState("save");
|
||||||
|
|
||||||
// In a real implementation, this would make a POST request to save the data
|
// In a real implementation, this would make a POST request to save the data
|
||||||
// logger.debug("Save data:", saveData);
|
// logger.debug("Save data:", saveData);
|
||||||
@@ -911,14 +745,14 @@ const useVideoTrimmer = () => {
|
|||||||
// Create the JSON data for saving as a copy
|
// Create the JSON data for saving as a copy
|
||||||
const saveData = {
|
const saveData = {
|
||||||
type: "save_as_a_copy",
|
type: "save_as_a_copy",
|
||||||
segments: sortedSegments.map(segment => ({
|
segments: sortedSegments.map((segment) => ({
|
||||||
startTime: formatDetailedTime(segment.startTime),
|
startTime: formatDetailedTime(segment.startTime),
|
||||||
endTime: formatDetailedTime(segment.endTime)
|
endTime: formatDetailedTime(segment.endTime)
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
// Display JSON in alert (for demonstration purposes)
|
// Display JSON in alert (for demonstration purposes)
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === "development") {
|
||||||
console.debug("Saving data as copy:", saveData);
|
console.debug("Saving data as copy:", saveData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -926,12 +760,12 @@ const useVideoTrimmer = () => {
|
|||||||
setHasUnsavedChanges(false);
|
setHasUnsavedChanges(false);
|
||||||
|
|
||||||
// Debug message
|
// Debug message
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === "development") {
|
||||||
console.debug("Changes saved as copy - reset unsaved changes flag");
|
console.debug("Changes saved as copy - reset unsaved changes flag");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to history with special "save_copy" action to mark saved state
|
// Save to history with special "save_copy" action to mark saved state
|
||||||
saveState('save_copy');
|
saveState("save_copy");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle save segments individually action
|
// Handle save segments individually action
|
||||||
@@ -942,7 +776,7 @@ const useVideoTrimmer = () => {
|
|||||||
// Create the JSON data for saving individual segments
|
// Create the JSON data for saving individual segments
|
||||||
const saveData = {
|
const saveData = {
|
||||||
type: "save_segments",
|
type: "save_segments",
|
||||||
segments: sortedSegments.map(segment => ({
|
segments: sortedSegments.map((segment) => ({
|
||||||
name: segment.name,
|
name: segment.name,
|
||||||
startTime: formatDetailedTime(segment.startTime),
|
startTime: formatDetailedTime(segment.startTime),
|
||||||
endTime: formatDetailedTime(segment.endTime)
|
endTime: formatDetailedTime(segment.endTime)
|
||||||
@@ -950,7 +784,7 @@ const useVideoTrimmer = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Display JSON in alert (for demonstration purposes)
|
// Display JSON in alert (for demonstration purposes)
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === "development") {
|
||||||
console.debug("Saving data as segments:", saveData);
|
console.debug("Saving data as segments:", saveData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -961,7 +795,7 @@ const useVideoTrimmer = () => {
|
|||||||
logger.debug("All segments saved individually - reset unsaved changes flag");
|
logger.debug("All segments saved individually - reset unsaved changes flag");
|
||||||
|
|
||||||
// Save to history with special "save_segments" action to mark saved state
|
// Save to history with special "save_segments" action to mark saved state
|
||||||
saveState('save_segments');
|
saveState("save_segments");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle seeking with mobile check
|
// Handle seeking with mobile check
|
||||||
@@ -973,7 +807,11 @@ const useVideoTrimmer = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Check if device is mobile
|
// Check if device is mobile
|
||||||
const isMobile = typeof window !== 'undefined' && /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(navigator.userAgent);
|
const isMobile =
|
||||||
|
typeof window !== "undefined" &&
|
||||||
|
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(
|
||||||
|
navigator.userAgent
|
||||||
|
);
|
||||||
|
|
||||||
// Add videoInitialized state
|
// Add videoInitialized state
|
||||||
const [videoInitialized, setVideoInitialized] = useState(false);
|
const [videoInitialized, setVideoInitialized] = useState(false);
|
||||||
@@ -1008,7 +846,7 @@ const useVideoTrimmer = () => {
|
|||||||
// If video is somehow paused, ensure it keeps playing
|
// If video is somehow paused, ensure it keeps playing
|
||||||
if (video.paused) {
|
if (video.paused) {
|
||||||
logger.debug("Ensuring playback continues to next segment");
|
logger.debug("Ensuring playback continues to next segment");
|
||||||
video.play().catch(err => {
|
video.play().catch((err) => {
|
||||||
console.error("Error continuing segment playback:", err);
|
console.error("Error continuing segment playback:", err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1017,12 +855,12 @@ const useVideoTrimmer = () => {
|
|||||||
video.pause();
|
video.pause();
|
||||||
setIsPlayingSegments(false);
|
setIsPlayingSegments(false);
|
||||||
setCurrentSegmentIndex(0);
|
setCurrentSegmentIndex(0);
|
||||||
video.removeEventListener('timeupdate', handleSegmentsPlayback);
|
video.removeEventListener("timeupdate", handleSegmentsPlayback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
video.addEventListener('timeupdate', handleSegmentsPlayback);
|
video.addEventListener("timeupdate", handleSegmentsPlayback);
|
||||||
|
|
||||||
// Start playing if not already playing
|
// Start playing if not already playing
|
||||||
if (video.paused && orderedSegments.length > 0) {
|
if (video.paused && orderedSegments.length > 0) {
|
||||||
@@ -1031,7 +869,7 @@ const useVideoTrimmer = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
video.removeEventListener('timeupdate', handleSegmentsPlayback);
|
video.removeEventListener("timeupdate", handleSegmentsPlayback);
|
||||||
};
|
};
|
||||||
}, [isPlayingSegments, currentSegmentIndex, clipSegments]);
|
}, [isPlayingSegments, currentSegmentIndex, clipSegments]);
|
||||||
|
|
||||||
@@ -1040,15 +878,20 @@ const useVideoTrimmer = () => {
|
|||||||
const handleSegmentIndexUpdate = (event: CustomEvent) => {
|
const handleSegmentIndexUpdate = (event: CustomEvent) => {
|
||||||
const { segmentIndex } = event.detail;
|
const { segmentIndex } = event.detail;
|
||||||
if (isPlayingSegments && segmentIndex !== currentSegmentIndex) {
|
if (isPlayingSegments && segmentIndex !== currentSegmentIndex) {
|
||||||
logger.debug(`Updating current segment index from ${currentSegmentIndex} to ${segmentIndex}`);
|
logger.debug(
|
||||||
|
`Updating current segment index from ${currentSegmentIndex} to ${segmentIndex}`
|
||||||
|
);
|
||||||
setCurrentSegmentIndex(segmentIndex);
|
setCurrentSegmentIndex(segmentIndex);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('update-segment-index', handleSegmentIndexUpdate as EventListener);
|
document.addEventListener("update-segment-index", handleSegmentIndexUpdate as EventListener);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('update-segment-index', handleSegmentIndexUpdate as EventListener);
|
document.removeEventListener(
|
||||||
|
"update-segment-index",
|
||||||
|
handleSegmentIndexUpdate as EventListener
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}, [isPlayingSegments, currentSegmentIndex]);
|
}, [isPlayingSegments, currentSegmentIndex]);
|
||||||
|
|
||||||
@@ -1067,10 +910,7 @@ const useVideoTrimmer = () => {
|
|||||||
setIsPlayingSegments(true);
|
setIsPlayingSegments(true);
|
||||||
setCurrentSegmentIndex(0);
|
setCurrentSegmentIndex(0);
|
||||||
|
|
||||||
// Exit preview mode if active
|
// Start segments playback
|
||||||
if (isPreviewMode) {
|
|
||||||
setIsPreviewMode(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort segments by start time
|
// Sort segments by start time
|
||||||
const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
||||||
@@ -1079,7 +919,7 @@ const useVideoTrimmer = () => {
|
|||||||
video.currentTime = orderedSegments[0].startTime;
|
video.currentTime = orderedSegments[0].startTime;
|
||||||
|
|
||||||
// Start playback with proper error handling
|
// Start playback with proper error handling
|
||||||
video.play().catch(err => {
|
video.play().catch((err) => {
|
||||||
console.error("Error starting segments playback:", err);
|
console.error("Error starting segments playback:", err);
|
||||||
setIsPlayingSegments(false);
|
setIsPlayingSegments(false);
|
||||||
});
|
});
|
||||||
@@ -1095,7 +935,6 @@ const useVideoTrimmer = () => {
|
|||||||
isPlaying,
|
isPlaying,
|
||||||
setIsPlaying,
|
setIsPlaying,
|
||||||
isMuted,
|
isMuted,
|
||||||
isPreviewMode,
|
|
||||||
isPlayingSegments,
|
isPlayingSegments,
|
||||||
thumbnails,
|
thumbnails,
|
||||||
trimStart,
|
trimStart,
|
||||||
@@ -1114,7 +953,6 @@ const useVideoTrimmer = () => {
|
|||||||
handleReset,
|
handleReset,
|
||||||
handleUndo,
|
handleUndo,
|
||||||
handleRedo,
|
handleRedo,
|
||||||
handlePreview,
|
|
||||||
handlePlaySegments,
|
handlePlaySegments,
|
||||||
toggleMute,
|
toggleMute,
|
||||||
handleSave,
|
handleSave,
|
||||||
@@ -1122,7 +960,7 @@ const useVideoTrimmer = () => {
|
|||||||
handleSaveSegments,
|
handleSaveSegments,
|
||||||
isMobile,
|
isMobile,
|
||||||
videoInitialized,
|
videoInitialized,
|
||||||
setVideoInitialized,
|
setVideoInitialized
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -125,13 +125,13 @@
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
background-color: #EEE; /* Very light gray background */
|
background-color: #eee; /* Very light gray background */
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-container {
|
.timeline-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: #EEE; /* Very light gray background */
|
background-color: #eee; /* Very light gray background */
|
||||||
height: 6rem;
|
height: 6rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -208,17 +208,27 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: box-shadow 0.2s, transform 0.1s;
|
transition:
|
||||||
|
box-shadow 0.2s,
|
||||||
|
transform 0.1s;
|
||||||
/* Original z-index for stacking order based on segment ID */
|
/* Original z-index for stacking order based on segment ID */
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* No background colors for segments, just borders with 2-color scheme */
|
/* No background colors for segments, just borders with 2-color scheme */
|
||||||
.clip-segment:nth-child(odd), .segment-color-1, .segment-color-3, .segment-color-5, .segment-color-7 {
|
.clip-segment:nth-child(odd),
|
||||||
|
.segment-color-1,
|
||||||
|
.segment-color-3,
|
||||||
|
.segment-color-5,
|
||||||
|
.segment-color-7 {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 2px solid rgba(0, 123, 255, 0.9); /* Blue border */
|
border: 2px solid rgba(0, 123, 255, 0.9); /* Blue border */
|
||||||
}
|
}
|
||||||
.clip-segment:nth-child(even), .segment-color-2, .segment-color-4, .segment-color-6, .segment-color-8 {
|
.clip-segment:nth-child(even),
|
||||||
|
.segment-color-2,
|
||||||
|
.segment-color-4,
|
||||||
|
.segment-color-6,
|
||||||
|
.segment-color-8 {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 2px solid rgba(108, 117, 125, 0.9); /* Gray border */
|
border: 2px solid rgba(108, 117, 125, 0.9); /* Gray border */
|
||||||
}
|
}
|
||||||
@@ -315,7 +325,7 @@
|
|||||||
input[type="range"] {
|
input[type="range"] {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
background: #E0E0E0;
|
background: #e0e0e0;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,12 +360,14 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]::after {
|
[data-tooltip]::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -366,7 +378,9 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,7 +478,7 @@ button[disabled][data-tooltip]::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.segment-tooltip::after {
|
.segment-tooltip::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -6px;
|
bottom: -6px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -539,7 +553,7 @@ button[disabled][data-tooltip]::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.empty-space-tooltip::after {
|
.empty-space-tooltip::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -8px;
|
bottom: -8px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -617,7 +631,9 @@ button[disabled][data-tooltip]::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Save buttons styling */
|
/* Save buttons styling */
|
||||||
.save-button, .save-copy-button, .save-segments-button {
|
.save-button,
|
||||||
|
.save-copy-button,
|
||||||
|
.save-segments-button {
|
||||||
background-color: rgba(0, 123, 255, 0.8);
|
background-color: rgba(0, 123, 255, 0.8);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -628,7 +644,8 @@ button[disabled][data-tooltip]::after {
|
|||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-button:hover, .save-copy-button:hover {
|
.save-button:hover,
|
||||||
|
.save-copy-button:hover {
|
||||||
background-color: rgba(0, 123, 255, 1);
|
background-color: rgba(0, 123, 255, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,7 +752,8 @@ button[disabled][data-tooltip]::after {
|
|||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-time, .duration-time {
|
.current-time,
|
||||||
|
.duration-time {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,7 +788,8 @@ button[disabled][data-tooltip]::after {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-button, .save-copy-button {
|
.save-button,
|
||||||
|
.save-copy-button {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const logger = {
|
|||||||
* Logs debug messages only in development environment
|
* Logs debug messages only in development environment
|
||||||
*/
|
*/
|
||||||
debug: (...args: any[]) => {
|
debug: (...args: any[]) => {
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === "development") {
|
||||||
console.debug(...args);
|
console.debug(...args);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ async function throwIfResNotOk(res: Response) {
|
|||||||
export async function apiRequest(
|
export async function apiRequest(
|
||||||
method: string,
|
method: string,
|
||||||
url: string,
|
url: string,
|
||||||
data?: unknown | undefined,
|
data?: unknown | undefined
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method,
|
method,
|
||||||
headers: data ? { "Content-Type": "application/json" } : {},
|
headers: data ? { "Content-Type": "application/json" } : {},
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
body: data ? JSON.stringify(data) : undefined,
|
||||||
credentials: "include",
|
credentials: "include"
|
||||||
});
|
});
|
||||||
|
|
||||||
await throwIfResNotOk(res);
|
await throwIfResNotOk(res);
|
||||||
@@ -24,13 +24,11 @@ export async function apiRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UnauthorizedBehavior = "returnNull" | "throw";
|
type UnauthorizedBehavior = "returnNull" | "throw";
|
||||||
export const getQueryFn: <T>(options: {
|
export const getQueryFn: <T>(options: { on401: UnauthorizedBehavior }) => QueryFunction<T> =
|
||||||
on401: UnauthorizedBehavior;
|
|
||||||
}) => QueryFunction<T> =
|
|
||||||
({ on401: unauthorizedBehavior }) =>
|
({ on401: unauthorizedBehavior }) =>
|
||||||
async ({ queryKey }) => {
|
async ({ queryKey }) => {
|
||||||
const res = await fetch(queryKey[0] as string, {
|
const res = await fetch(queryKey[0] as string, {
|
||||||
credentials: "include",
|
credentials: "include"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (unauthorizedBehavior === "returnNull" && res.status === 401) {
|
if (unauthorizedBehavior === "returnNull" && res.status === 401) {
|
||||||
@@ -48,10 +46,10 @@ export const queryClient = new QueryClient({
|
|||||||
refetchInterval: false,
|
refetchInterval: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
retry: false,
|
retry: false
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
retry: false,
|
retry: false
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { clsx, type ClassValue } from "clsx"
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,7 @@
|
|||||||
* Generate a solid color background for a segment
|
* Generate a solid color background for a segment
|
||||||
* Returns a CSS color based on the segment position
|
* Returns a CSS color based on the segment position
|
||||||
*/
|
*/
|
||||||
export const generateSolidColor = (
|
export const generateSolidColor = (time: number, duration: number): string => {
|
||||||
time: number,
|
|
||||||
duration: number
|
|
||||||
): string => {
|
|
||||||
// Use the time position to create different colors
|
// Use the time position to create different colors
|
||||||
// This gives each segment a different color without needing an image
|
// This gives each segment a different color without needing an image
|
||||||
const position = Math.min(Math.max(time / (duration || 1), 0), 1);
|
const position = Math.min(Math.max(time / (duration || 1), 0), 1);
|
||||||
@@ -29,11 +26,11 @@ export const generateThumbnail = async (
|
|||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// Create a small canvas for the solid color
|
// Create a small canvas for the solid color
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = 10; // Much smaller - we only need a color
|
canvas.width = 10; // Much smaller - we only need a color
|
||||||
canvas.height = 10;
|
canvas.height = 10;
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext("2d");
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
// Get the solid color based on time
|
// Get the solid color based on time
|
||||||
const color = generateSolidColor(time, videoElement.duration);
|
const color = generateSolidColor(time, videoElement.duration);
|
||||||
@@ -44,7 +41,7 @@ export const generateThumbnail = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert to data URL (much smaller now)
|
// Convert to data URL (much smaller now)
|
||||||
const dataUrl = canvas.toDataURL('image/png', 0.5);
|
const dataUrl = canvas.toDataURL("image/png", 0.5);
|
||||||
resolve(dataUrl);
|
resolve(dataUrl);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createRoot } from "react-dom/client";
|
|||||||
import App from "./App";
|
import App from "./App";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
window.MEDIA_DATA = {
|
window.MEDIA_DATA = {
|
||||||
videoUrl: "",
|
videoUrl: "",
|
||||||
mediaId: ""
|
mediaId: ""
|
||||||
@@ -30,8 +30,8 @@ const mountComponents = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener('DOMContentLoaded', mountComponents);
|
document.addEventListener("DOMContentLoaded", mountComponents);
|
||||||
} else {
|
} else {
|
||||||
mountComponents();
|
mountComponents();
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ interface TrimVideoResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to simulate delay
|
// Helper function to simulate delay
|
||||||
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
// For now, we'll use a mock API that returns a promise
|
// For now, we'll use a mock API that returns a promise
|
||||||
// This can be replaced with actual API calls later
|
// This can be replaced with actual API calls later
|
||||||
@@ -29,8 +29,8 @@ export const trimVideo = async (
|
|||||||
try {
|
try {
|
||||||
// Attempt the real API call
|
// Attempt the real API call
|
||||||
const response = await fetch(`/api/v1/media/${mediaId}/trim_video`, {
|
const response = await fetch(`/api/v1/media/${mediaId}/trim_video`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,13 +21,15 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:after {
|
[data-tooltip]:after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -37,7 +39,9 @@
|
|||||||
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
|
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +147,9 @@
|
|||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s, color 0.2s;
|
transition:
|
||||||
|
background-color 0.2s,
|
||||||
|
color 0.2s;
|
||||||
min-width: auto;
|
min-width: auto;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -163,12 +169,28 @@
|
|||||||
color: rgba(51, 51, 51, 0.7);
|
color: rgba(51, 51, 51, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.segment-color-1 { background-color: rgba(59, 130, 246, 0.15); }
|
.segment-color-1 {
|
||||||
.segment-color-2 { background-color: rgba(16, 185, 129, 0.15); }
|
background-color: rgba(59, 130, 246, 0.15);
|
||||||
.segment-color-3 { background-color: rgba(245, 158, 11, 0.15); }
|
}
|
||||||
.segment-color-4 { background-color: rgba(239, 68, 68, 0.15); }
|
.segment-color-2 {
|
||||||
.segment-color-5 { background-color: rgba(139, 92, 246, 0.15); }
|
background-color: rgba(16, 185, 129, 0.15);
|
||||||
.segment-color-6 { background-color: rgba(236, 72, 153, 0.15); }
|
}
|
||||||
.segment-color-7 { background-color: rgba(6, 182, 212, 0.15); }
|
.segment-color-3 {
|
||||||
.segment-color-8 { background-color: rgba(250, 204, 21, 0.15); }
|
background-color: rgba(245, 158, 11, 0.15);
|
||||||
|
}
|
||||||
|
.segment-color-4 {
|
||||||
|
background-color: rgba(239, 68, 68, 0.15);
|
||||||
|
}
|
||||||
|
.segment-color-5 {
|
||||||
|
background-color: rgba(139, 92, 246, 0.15);
|
||||||
|
}
|
||||||
|
.segment-color-6 {
|
||||||
|
background-color: rgba(236, 72, 153, 0.15);
|
||||||
|
}
|
||||||
|
.segment-color-7 {
|
||||||
|
background-color: rgba(6, 182, 212, 0.15);
|
||||||
|
}
|
||||||
|
.segment-color-8 {
|
||||||
|
background-color: rgba(250, 204, 21, 0.15);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
#video-editor-trim-root {
|
#video-editor-trim-root {
|
||||||
|
|
||||||
/* Tooltip styles - only on desktop where hover is available */
|
/* Tooltip styles - only on desktop where hover is available */
|
||||||
@media (hover: hover) and (pointer: fine) {
|
@media (hover: hover) and (pointer: fine) {
|
||||||
[data-tooltip] {
|
[data-tooltip] {
|
||||||
@@ -22,13 +21,15 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:after {
|
[data-tooltip]:after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -38,7 +39,9 @@
|
|||||||
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
|
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +125,6 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
min-width: auto;
|
min-width: auto;
|
||||||
|
|
||||||
/* Disabled hover effect as requested */
|
|
||||||
&:hover:not(:disabled) {
|
&:hover:not(:disabled) {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
@@ -147,7 +149,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Style for play buttons with highlight effect */
|
/* Style for play buttons with highlight effect */
|
||||||
.play-button, .preview-button {
|
.play-button,
|
||||||
|
.preview-button {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -185,7 +188,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Completely disable ALL hover effects for play buttons */
|
/* Completely disable ALL hover effects for play buttons */
|
||||||
.play-button:hover:not(:disabled), .preview-button:hover:not(:disabled) {
|
.play-button:hover:not(:disabled),
|
||||||
|
.preview-button:hover:not(:disabled) {
|
||||||
/* Reset everything to prevent any changes */
|
/* Reset everything to prevent any changes */
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
transform: none !important;
|
transform: none !important;
|
||||||
@@ -194,26 +198,14 @@
|
|||||||
background: none !important;
|
background: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.play-button svg, .preview-button svg {
|
.play-button svg,
|
||||||
|
.preview-button svg {
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
/* Make sure SVG scales with the button but doesn't change layout */
|
/* Make sure SVG scales with the button but doesn't change layout */
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style for the preview mode message that replaces the play button */
|
|
||||||
.preview-mode-message {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background-color: rgba(59, 130, 246, 0.1);
|
|
||||||
color: #3b82f6;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
@@ -226,13 +218,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-mode-message svg {
|
|
||||||
height: 1.25rem;
|
|
||||||
width: 1.25rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add responsive button text class */
|
/* Add responsive button text class */
|
||||||
.button-text {
|
.button-text {
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
@@ -257,7 +242,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Keep font size consistent regardless of screen size */
|
/* Keep font size consistent regardless of screen size */
|
||||||
.preview-button, .play-button {
|
.preview-button,
|
||||||
|
.play-button {
|
||||||
font-size: 0.875rem !important;
|
font-size: 0.875rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -340,12 +326,6 @@
|
|||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smaller preview mode message */
|
|
||||||
.preview-mode-message {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
padding: 4px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
margin: 0 0.25rem;
|
margin: 0 0.25rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#video-editor-trim-root {
|
#video-editor-trim-root {
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -10,9 +10,9 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-container {
|
.modal-container {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
@@ -21,9 +21,9 @@
|
|||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
animation: modal-fade-in 0.3s ease-out;
|
animation: modal-fade-in 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes modal-fade-in {
|
@keyframes modal-fade-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-20px);
|
transform: translateY(-20px);
|
||||||
@@ -32,24 +32,24 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-title {
|
.modal-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close-button {
|
.modal-close-button {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -59,112 +59,116 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close-button:hover {
|
.modal-close-button:hover {
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-actions {
|
.modal-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
border-top: 1px solid #eee;
|
border-top: 1px solid #eee;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-button {
|
.modal-button {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-button-primary {
|
.modal-button-primary {
|
||||||
background-color: #0066cc;
|
background-color: #0066cc;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-button-primary:hover {
|
.modal-button-primary:hover {
|
||||||
background-color: #0055aa;
|
background-color: #0055aa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-button-secondary {
|
.modal-button-secondary {
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-button-secondary:hover {
|
.modal-button-secondary:hover {
|
||||||
background-color: #e0e0e0;
|
background-color: #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-button-danger {
|
.modal-button-danger {
|
||||||
background-color: #dc3545;
|
background-color: #dc3545;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-button-danger:hover {
|
.modal-button-danger:hover {
|
||||||
background-color: #bd2130;
|
background-color: #bd2130;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal content styles */
|
/* Modal content styles */
|
||||||
.modal-message {
|
.modal-message {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-spinner {
|
.modal-spinner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
border: 4px solid rgba(0, 0, 0, 0.1);
|
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border-top: 4px solid #0066cc;
|
border-top: 4px solid #0066cc;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.modal-success-icon {
|
.modal-success-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
color: #28a745;
|
color: #28a745;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-success-icon svg {
|
.modal-success-icon svg {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
color: #4CAF50;
|
color: #4caf50;
|
||||||
animation: success-pop 0.5s ease-out;
|
animation: success-pop 0.5s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes success-pop {
|
@keyframes success-pop {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(0);
|
transform: scale(0);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -177,24 +181,24 @@
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-error-icon {
|
.modal-error-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
color: #dc3545;
|
color: #dc3545;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-error-icon svg {
|
.modal-error-icon svg {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
color: #F44336;
|
color: #f44336;
|
||||||
animation: error-pop 0.5s ease-out;
|
animation: error-pop 0.5s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes error-pop {
|
@keyframes error-pop {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(0);
|
transform: scale(0);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -207,16 +211,16 @@
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-choices {
|
.modal-choices {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-choice-button {
|
.modal-choice-button {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -230,39 +234,39 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-choice-button:hover {
|
.modal-choice-button:hover {
|
||||||
background-color: #0055aa;
|
background-color: #0055aa;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-choice-button svg {
|
.modal-choice-button svg {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-link {
|
.success-link {
|
||||||
background-color: #4CAF50;
|
background-color: #4caf50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-link:hover {
|
.success-link:hover {
|
||||||
background-color: #3d8b40;
|
background-color: #3d8b40;
|
||||||
}
|
}
|
||||||
|
|
||||||
.centered-choice {
|
.centered-choice {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: auto;
|
width: auto;
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
background-color: #0066cc;
|
background-color: #0066cc;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.centered-choice:hover {
|
.centered-choice:hover {
|
||||||
background-color: #0055aa;
|
background-color: #0055aa;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.modal-container {
|
.modal-container {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
}
|
}
|
||||||
@@ -274,29 +278,29 @@
|
|||||||
.modal-button {
|
.modal-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
color: #F44336;
|
color: #f44336;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
background-color: rgba(244, 67, 54, 0.1);
|
background-color: rgba(244, 67, 54, 0.1);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border-left: 4px solid #F44336;
|
border-left: 4px solid #f44336;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.redirect-message {
|
.redirect-message {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
color: #555;
|
color: #555;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.countdown {
|
.countdown {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #0066cc;
|
color: #0066cc;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,7 +321,7 @@
|
|||||||
|
|
||||||
.segment-tooltip:after,
|
.segment-tooltip:after,
|
||||||
.empty-space-tooltip:after {
|
.empty-space-tooltip:after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -5px;
|
bottom: -5px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -335,7 +335,7 @@
|
|||||||
|
|
||||||
.segment-tooltip:before,
|
.segment-tooltip:before,
|
||||||
.empty-space-tooltip:before {
|
.empty-space-tooltip:before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -6px;
|
bottom: -6px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -438,7 +438,7 @@
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-right: 0.50rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-button:hover {
|
.time-button:hover {
|
||||||
@@ -612,13 +612,15 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:after {
|
[data-tooltip]:after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -628,7 +630,9 @@
|
|||||||
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
|
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,27 +673,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-success-icon svg {
|
.modal-success-icon svg {
|
||||||
color: #4CAF50;
|
color: #4caf50;
|
||||||
animation: fadeIn 0.5s ease-in-out;
|
animation: fadeIn 0.5s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-error-icon svg {
|
.modal-error-icon svg {
|
||||||
color: #F44336;
|
color: #f44336;
|
||||||
animation: fadeIn 0.5s ease-in-out;
|
animation: fadeIn 0.5s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-link {
|
.success-link {
|
||||||
background-color: #4CAF50;
|
background-color: #4caf50;
|
||||||
color: white;
|
color: white;
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-link:hover {
|
.success-link:hover {
|
||||||
background-color: #388E3C;
|
background-color: #388e3c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
color: #F44336;
|
color: #f44336;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -809,47 +813,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% { opacity: 0.7; transform: scale(1); }
|
0% {
|
||||||
50% { opacity: 1; transform: scale(1.05); }
|
opacity: 0.7;
|
||||||
100% { opacity: 0.7; transform: scale(1); }
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
50% {
|
||||||
/* Preview mode styles */
|
opacity: 1;
|
||||||
.preview-mode .tooltip-action-btn {
|
transform: scale(1.05);
|
||||||
opacity: 0.5;
|
}
|
||||||
pointer-events: none;
|
100% {
|
||||||
cursor: not-allowed;
|
opacity: 0.7;
|
||||||
}
|
transform: scale(1);
|
||||||
|
}
|
||||||
.preview-mode .tooltip-time-btn {
|
|
||||||
opacity: 0.5;
|
|
||||||
pointer-events: none;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Timeline preview mode styles */
|
|
||||||
.timeline-container-card.preview-mode {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-container-card.preview-mode .timeline-marker-head,
|
|
||||||
.timeline-container-card.preview-mode .timeline-marker-drag,
|
|
||||||
.timeline-container-card.preview-mode .clip-segment,
|
|
||||||
.timeline-container-card.preview-mode .clip-segment-handle,
|
|
||||||
.timeline-container-card.preview-mode .time-button,
|
|
||||||
.timeline-container-card.preview-mode .zoom-button,
|
|
||||||
.timeline-container-card.preview-mode .save-button,
|
|
||||||
.timeline-container-card.preview-mode .save-copy-button,
|
|
||||||
.timeline-container-card.preview-mode .save-segments-button {
|
|
||||||
opacity: 0.5;
|
|
||||||
pointer-events: none;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-container-card.preview-mode .clip-segment:hover {
|
|
||||||
box-shadow: none;
|
|
||||||
border-color: rgba(0, 0, 0, 0.15);
|
|
||||||
background-color: inherit !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Segments playback mode styles - minimal functional styling */
|
/* Segments playback mode styles - minimal functional styling */
|
||||||
@@ -858,19 +833,26 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.segments-playback-mode .tooltip-action-btn.set-in,
|
|
||||||
.segments-playback-mode .tooltip-action-btn.set-out,
|
|
||||||
.segments-playback-mode .tooltip-action-btn.play-from-start {
|
|
||||||
opacity: 0.5;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.segments-playback-mode .tooltip-action-btn.play,
|
.segments-playback-mode .tooltip-action-btn.play,
|
||||||
.segments-playback-mode .tooltip-action-btn.pause {
|
.segments-playback-mode .tooltip-action-btn.pause {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* During segments playback mode, disable button interactions but keep hover working */
|
||||||
|
.segments-playback-mode .tooltip-time-btn[disabled],
|
||||||
|
.segments-playback-mode .tooltip-action-btn[disabled] {
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure disabled buttons still show tooltips on hover */
|
||||||
|
.segments-playback-mode [data-tooltip][disabled]:hover:before,
|
||||||
|
.segments-playback-mode [data-tooltip][disabled]:hover:after {
|
||||||
|
opacity: 1 !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Show segments playback message */
|
/* Show segments playback message */
|
||||||
.segments-playback-message {
|
.segments-playback-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -56,6 +56,26 @@
|
|||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Disabled state for time display */
|
||||||
|
.tooltip-time-display.disabled {
|
||||||
|
pointer-events: none !important;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
opacity: 0.6 !important;
|
||||||
|
user-select: none !important;
|
||||||
|
-webkit-user-select: none !important;
|
||||||
|
-moz-user-select: none !important;
|
||||||
|
-ms-user-select: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force disabled tooltips to show on hover for better user feedback */
|
||||||
|
.tooltip-time-btn.disabled[data-tooltip]:hover:before,
|
||||||
|
.tooltip-time-btn.disabled[data-tooltip]:hover:after,
|
||||||
|
.tooltip-action-btn.disabled[data-tooltip]:hover:before,
|
||||||
|
.tooltip-action-btn.disabled[data-tooltip]:hover:after {
|
||||||
|
opacity: 1 !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
.tooltip-actions {
|
.tooltip-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -100,14 +120,16 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
z-index: 2500; /* High z-index */
|
z-index: 2500; /* High z-index */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Triangle arrow pointing up to the button */
|
/* Triangle arrow pointing up to the button */
|
||||||
.tooltip-action-btn[data-tooltip]:after {
|
.tooltip-action-btn[data-tooltip]:after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 35px; /* Match the before element */
|
top: 35px; /* Match the before element */
|
||||||
left: 50%; /* Center horizontally */
|
left: 50%; /* Center horizontally */
|
||||||
@@ -119,7 +141,9 @@
|
|||||||
margin-left: 0; /* Reset margin */
|
margin-left: 0; /* Reset margin */
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
z-index: 2500; /* High z-index */
|
z-index: 2500; /* High z-index */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -227,6 +251,43 @@
|
|||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ensure pause button is properly styled when disabled */
|
||||||
|
.tooltip-action-btn.pause.disabled {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-action-btn.pause.disabled:hover {
|
||||||
|
background-color: #f3f4f6 !important;
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure play button is properly styled when disabled */
|
||||||
|
.tooltip-action-btn.play.disabled {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-action-btn.play.disabled:hover {
|
||||||
|
background-color: #f3f4f6 !important;
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure time adjustment buttons are properly styled when disabled */
|
||||||
|
.tooltip-time-btn.disabled {
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
background-color: #f3f4f6 !important;
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-time-btn.disabled:hover {
|
||||||
|
background-color: #f3f4f6 !important;
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Additional mobile optimizations */
|
/* Additional mobile optimizations */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.two-row-tooltip {
|
.two-row-tooltip {
|
||||||
|
|||||||
@@ -21,13 +21,15 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:after {
|
[data-tooltip]:after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -37,7 +39,9 @@
|
|||||||
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
|
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +116,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.play-pause-indicator::before {
|
.play-pause-indicator::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -160,9 +164,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% { opacity: 0.7; transform: scale(1); }
|
0% {
|
||||||
50% { opacity: 1; transform: scale(1.05); }
|
opacity: 0.7;
|
||||||
100% { opacity: 0.7; transform: scale(1); }
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-controls {
|
.video-controls {
|
||||||
@@ -232,7 +245,10 @@
|
|||||||
background-color: #ff0000;
|
background-color: #ff0000;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
transition: transform 0.1s ease, width 0.1s ease, height 0.1s ease;
|
transition:
|
||||||
|
transform 0.1s ease,
|
||||||
|
width 0.1s ease,
|
||||||
|
height 0.1s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make the scrubber larger when dragging for better control */
|
/* Make the scrubber larger when dragging for better control */
|
||||||
@@ -258,7 +274,7 @@
|
|||||||
|
|
||||||
/* Create a larger invisible touch target */
|
/* Create a larger invisible touch target */
|
||||||
.video-scrubber:before {
|
.video-scrubber:before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -10px;
|
top: -10px;
|
||||||
left: -10px;
|
left: -10px;
|
||||||
@@ -312,7 +328,7 @@
|
|||||||
|
|
||||||
/* Add a small arrow to the tooltip */
|
/* Add a small arrow to the tooltip */
|
||||||
.video-time-tooltip:after {
|
.video-time-tooltip:after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -4px;
|
bottom: -4px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"start": "NODE_ENV=production node dist/index.js",
|
"start": "NODE_ENV=production node dist/index.js",
|
||||||
"check": "tsc",
|
"check": "tsc",
|
||||||
"build:django": "vite build --config vite.video-editor.config.ts --outDir ../../../static/video_editor"
|
"build:django": "vite build --config vite.video-editor.config.ts --outDir ../../../static/video_editor",
|
||||||
|
"format": "npx prettier --write client/src/**/*.{ts,tsx,css}"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.74.4",
|
"@tanstack/react-query": "^5.74.4",
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
|
"prettier": "^3.6.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^5.4.18"
|
"vite": "^5.4.18"
|
||||||
|
|||||||
@@ -1834,6 +1834,11 @@ postcss@^8.4.43, postcss@^8.4.47:
|
|||||||
picocolors "^1.1.1"
|
picocolors "^1.1.1"
|
||||||
source-map-js "^1.2.1"
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
|
prettier@^3.6.0:
|
||||||
|
version "3.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.0.tgz#18ec98d62cb0757a5d4eab40253ff3e6d0fc8dea"
|
||||||
|
integrity sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw==
|
||||||
|
|
||||||
proxy-addr@~2.0.7:
|
proxy-addr@~2.0.7:
|
||||||
version "2.0.7"
|
version "2.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
|
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
|
||||||
@@ -2087,6 +2092,7 @@ statuses@2.0.1:
|
|||||||
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
|
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
|
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
|
||||||
|
name string-width-cjs
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
@@ -2105,6 +2111,7 @@ string-width@^5.0.1, string-width@^5.1.2:
|
|||||||
strip-ansi "^7.0.1"
|
strip-ansi "^7.0.1"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
|
name strip-ansi-cjs
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ Django==5.1.6
|
|||||||
djangorestframework==3.15.2
|
djangorestframework==3.15.2
|
||||||
python3-saml==1.16.0
|
python3-saml==1.16.0
|
||||||
django-allauth==65.4.1
|
django-allauth==65.4.1
|
||||||
psycopg==3.2.4
|
psycopg[pool]==3.2.4
|
||||||
uwsgi==2.0.28
|
uwsgi==2.0.28
|
||||||
django-redis==5.4.0
|
django-redis==5.4.0
|
||||||
celery==5.4.0
|
celery==5.4.0
|
||||||
@@ -18,7 +18,6 @@ requests==2.32.3
|
|||||||
django-celery-email==3.0.0
|
django-celery-email==3.0.0
|
||||||
m3u8==6.0.0
|
m3u8==6.0.0
|
||||||
django-debug-toolbar==5.0.1
|
django-debug-toolbar==5.0.1
|
||||||
django-login-required-middleware==0.9.0
|
|
||||||
pre-commit==4.1.0
|
pre-commit==4.1.0
|
||||||
django-jazzmin==3.0.1
|
django-jazzmin==3.0.1
|
||||||
pysubs2==1.8.0
|
pysubs2==1.8.0
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
<script src="{% static "js/_commons.js" %}"></script>
|
<script src="{% static "js/_commons.js" %}?v={{ VERSION }}"></script>
|
||||||
@@ -22,10 +22,10 @@
|
|||||||
<link href="{% static "lib/gfonts/gfonts.css" %}" rel="stylesheet">
|
<link href="{% static "lib/gfonts/gfonts.css" %}" rel="stylesheet">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<link href="{% static "css/_commons.css" %}" rel="preload" as="style">
|
<link href="{% static "css/_commons.css" %}?v={{ VERSION }}" rel="preload" as="style">
|
||||||
<link href="{% static "css/_commons.css" %}" rel="stylesheet">
|
<link href="{% static "css/_commons.css" %}?v={{ VERSION }}" rel="stylesheet">
|
||||||
|
|
||||||
<link href="{% static "css/_extra.css" %}" rel="preload" as="style">
|
<link href="{% static "css/_extra.css" %}?v={{ VERSION }}" rel="preload" as="style">
|
||||||
<link href="{% static "css/_extra.css" %}" rel="stylesheet">
|
<link href="{% static "css/_extra.css" %}?v={{ VERSION }}" rel="stylesheet">
|
||||||
|
|
||||||
<link href="{% static "js/_commons.js" %}" rel="preload" as="script">
|
<link href="{% static "js/_commons.js" %}?v={{ VERSION }}" rel="preload" as="script">
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ class TestX(TestCase):
|
|||||||
self.assertEqual(Media.objects.filter(media_type='image').count(), 1, "Media identification failed")
|
self.assertEqual(Media.objects.filter(media_type='image').count(), 1, "Media identification failed")
|
||||||
self.assertEqual(Media.objects.filter(user=self.user).count(), 3, "User assignment failed")
|
self.assertEqual(Media.objects.filter(user=self.user).count(), 3, "User assignment failed")
|
||||||
medium_video = Media.objects.get(title="medium_video.mp4")
|
medium_video = Media.objects.get(title="medium_video.mp4")
|
||||||
self.assertEqual(len(medium_video.hls_info), 11, "Problem with HLS info")
|
self.assertEqual(len(medium_video.hls_info), 13, "Problem with HLS info")
|
||||||
|
|
||||||
# using the provided EncodeProfiles, these two files should produce 9 Encoding objects.
|
# using the provided EncodeProfiles, these two files should produce 9 Encoding objects.
|
||||||
# if new EncodeProfiles are added and enabled, this will break!
|
# if new EncodeProfiles are added and enabled, this will break!
|
||||||
self.assertEqual(Encoding.objects.filter(status='success').count(), 9, "Not all video transcodings finished well")
|
self.assertEqual(Encoding.objects.filter(status='success').count(), 10, "Not all video transcodings finished well")
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ class TestFixtures(TestCase):
|
|||||||
profiles = EncodeProfile.objects.all()
|
profiles = EncodeProfile.objects.all()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
profiles.count(),
|
profiles.count(),
|
||||||
22,
|
23,
|
||||||
"Problem with Encode Profile fixtures",
|
"Problem with Encode Profile fixtures",
|
||||||
)
|
)
|
||||||
profiles = EncodeProfile.objects.filter(active=True)
|
profiles = EncodeProfile.objects.filter(active=True)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
profiles.count(),
|
profiles.count(),
|
||||||
6,
|
7,
|
||||||
"Problem with Encode Profile fixtures, not as active as expected",
|
"Problem with Encode Profile fixtures, not as active as expected",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ def import_class(path):
|
|||||||
path_bits = path.split(".")
|
path_bits = path.split(".")
|
||||||
|
|
||||||
if len(path_bits) < 2:
|
if len(path_bits) < 2:
|
||||||
message = "'{0}' is not a complete Python path.".format(path)
|
message = f"'{path}' is not a complete Python path."
|
||||||
raise ImproperlyConfigured(message)
|
raise ImproperlyConfigured(message)
|
||||||
|
|
||||||
class_name = path_bits.pop()
|
class_name = path_bits.pop()
|
||||||
@@ -15,7 +15,7 @@ def import_class(path):
|
|||||||
module_itself = import_module(module_path)
|
module_itself = import_module(module_path)
|
||||||
|
|
||||||
if not hasattr(module_itself, class_name):
|
if not hasattr(module_itself, class_name):
|
||||||
message = "The Python module '{}' has no '{}' class.".format(module_path, class_name)
|
message = f"The Python module '{module_path}' has no '{class_name}' class."
|
||||||
raise ImportError(message)
|
raise ImportError(message)
|
||||||
|
|
||||||
return getattr(module_itself, class_name)
|
return getattr(module_itself, class_name)
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class User(AbstractUser):
|
|||||||
ret = {}
|
ret = {}
|
||||||
results = []
|
results = []
|
||||||
ret["results"] = results
|
ret["results"] = results
|
||||||
ret["user_media"] = "/api/v1/media?author={0}".format(self.username)
|
ret["user_media"] = f"/api/v1/media?author={self.username}"
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
@@ -210,7 +210,7 @@ class Channel(models.Model):
|
|||||||
super(Channel, self).save(*args, **kwargs)
|
super(Channel, self).save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{0} -{1}".format(self.user.username, self.title)
|
return f"{self.user.username} -{self.title}"
|
||||||
|
|
||||||
def get_absolute_url(self, edit=False):
|
def get_absolute_url(self, edit=False):
|
||||||
if edit:
|
if edit:
|
||||||
@@ -230,7 +230,7 @@ def post_user_create(sender, instance, created, **kwargs):
|
|||||||
new = Channel.objects.create(title="default", user=instance)
|
new = Channel.objects.create(title="default", user=instance)
|
||||||
new.save()
|
new.save()
|
||||||
if settings.ADMINS_NOTIFICATIONS.get("NEW_USER", False):
|
if settings.ADMINS_NOTIFICATIONS.get("NEW_USER", False):
|
||||||
title = "[{}] - New user just registered".format(settings.PORTAL_NAME)
|
title = f"[{settings.PORTAL_NAME}] - New user just registered"
|
||||||
msg = """
|
msg = """
|
||||||
User has just registered with email %s\n
|
User has just registered with email %s\n
|
||||||
Visit user profile page at %s
|
Visit user profile page at %s
|
||||||
|
|||||||
Reference in New Issue
Block a user