mirror of
https://github.com/ZiZc3/VLC.git
synced 2026-10-18 18:02:32 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3253020a6 | ||
|
|
4d80319bb9 | ||
|
|
e652a10d78 | ||
|
|
72f186e209 | ||
|
|
38c6fcb250 | ||
|
|
c6d14e166e |
No files matched your search
@@ -82,7 +82,7 @@ Optional full access. With etaHEN, OnionHEN or the Helper, VLC can lift its sand
|
||||
| `/mnt/usb0` … `/mnt/usb7`, `/mnt/ext0` | USB and extended storage |
|
||||
| `/data/homebrew/PPSA85300/vlc-ps5.log` | Log of the last session (attach it when reporting a problem) |
|
||||
|
||||
## VLC Features
|
||||
## All VLC Features
|
||||
|
||||
✅ In the app.
|
||||
|
||||
|
||||
Binary file not shown.
+49
-10
@@ -146,13 +146,11 @@ About VLC = حول VLC
|
||||
PS5 port = نسخة PS5
|
||||
VideoLAN and the VLC contributors = VideoLAN والمساهمون في VLC
|
||||
Built with = بُني باستخدام
|
||||
GPLv2+. An unofficial port, not affiliated with VideoLAN or Sony. = GPLv2+. نسخة غير رسمية، لا علاقة لها بـ VideoLAN أو Sony.
|
||||
GPL-3.0. An unofficial port, not affiliated with VideoLAN or Sony. = GPL-3.0. نسخة غير رسمية، لا علاقة لها بـ VideoLAN أو Sony.
|
||||
Resume = استئناف
|
||||
Audio, subtitles and more = الصوت والترجمة والمزيد
|
||||
Phone remote = التحكم من الهاتف
|
||||
Settings = الإعدادات
|
||||
Stop playback = إيقاف التشغيل
|
||||
Send files from a phone = إرسال ملفات من الهاتف
|
||||
Rescan media = إعادة فحص الوسائط
|
||||
Options = الخيارات
|
||||
Found %zu files = عُثر على %zu ملف
|
||||
@@ -188,7 +186,7 @@ Writes VLC's warnings and stats to vlc-ps5.log, for bug reports = يكتب تح
|
||||
What VLC can read on this console = ما يستطيع VLC قراءته على هذا الجهاز
|
||||
Starts VLC again: it then sees drives plugged in since (when sandboxed) = يعيد تشغيل VLC: فيرى الوحدات الموصلة بعد ذلك (في الوضع المعزول)
|
||||
Version, credits and licences = الإصدار والشكر والتراخيص
|
||||
The page that sends files and works as a remote, on your home network = الصفحة التي ترسل الملفات وتعمل كجهاز تحكم، على شبكتك المنزلية
|
||||
The page that sends files and takes your OpenSubtitles key, on your home network = الصفحة التي ترسل الملفات وتستقبل مفتاح OpenSubtitles، على شبكتك المنزلية
|
||||
Classic: panels and folders like a desktop player. Modern: black, VLC 4 style. VLC restarts = كلاسيكي: ألواح ومجلدات كمشغل سطح المكتب. حديث: أسود بنمط VLC 4. يُعاد تشغيل VLC
|
||||
Your own free OpenSubtitles API key (and account), here or from the phone page = مفتاح OpenSubtitles API المجاني الخاص بك (والحساب)، هنا أو من صفحة الهاتف
|
||||
The language of VLC's menus = لغة قوائم VLC
|
||||
@@ -228,16 +226,13 @@ Back right = خلفي يمين
|
||||
Each speaker plays a tone in turn. If one comes from the wrong place, tell us which. = كل سماعة تصدر نغمة بالتناوب. إن جاءت إحداها من مكان خاطئ، أخبرنا أيها.
|
||||
Stop going round = إيقاف الجولة
|
||||
Go round = بدء الجولة
|
||||
Send files from your phone = أرسل ملفات من هاتفك
|
||||
Phone access is off. Turn it on in Settings > System. = الوصول من الهاتف متوقف. فعّله من الإعدادات > النظام.
|
||||
The PS5 isn't connected to a network. Connect it, then come back. = جهاز PS5 غير متصل بشبكة. صِله ثم عد.
|
||||
On a phone or computer on the same Wi-Fi, open this address in the browser: = على هاتف أو حاسوب في شبكة Wi-Fi نفسها، افتح هذا العنوان في المتصفح:
|
||||
Send videos, music and subtitles to the PS5 = أرسل الفيديو والموسيقى والترجمات إلى PS5
|
||||
Use it as a remote while something plays = استخدمه كجهاز تحكم أثناء التشغيل
|
||||
Add your OpenSubtitles API key = أضف مفتاح OpenSubtitles API الخاص بك
|
||||
Receiving %s = جارٍ استلام %s
|
||||
Volume %d%% = الصوت %d%%
|
||||
OpenSubtitles key received from your phone = استُلم مفتاح OpenSubtitles من هاتفك
|
||||
A file arrived from your phone = وصل ملف من هاتفك
|
||||
VLC media folder = مجلد وسائط VLC
|
||||
USB drive %d = وحدة USB %d
|
||||
USB drive (%s) = وحدة USB (%s)
|
||||
@@ -387,12 +382,10 @@ The API key works = مفتاح API يعمل
|
||||
Checking the key = التحقق من المفتاح
|
||||
The server didn't answer. = لم يرد الخادم.
|
||||
Couldn't open this place. = تعذر فتح هذا المكان.
|
||||
File = الملف
|
||||
Folder = المجلد
|
||||
Bitrate = معدل البت
|
||||
Container = الحاوية
|
||||
Video = الفيديو
|
||||
Track = مسار
|
||||
White = أبيض
|
||||
Yellow = أصفر
|
||||
Cyan = سماوي
|
||||
@@ -512,3 +505,49 @@ Skip = تخطٍّ
|
||||
Start = ابدأ
|
||||
Run setup again = إعادة الإعداد الأولي
|
||||
Language, look and playback, chosen again = اختيار اللغة والمظهر والتشغيل من جديد
|
||||
Send files from a phone or PC = إرسال ملفات من الهاتف أو الكمبيوتر
|
||||
Send files from your phone or PC = أرسل ملفات من هاتفك أو الكمبيوتر
|
||||
OpenSubtitles key received from your phone or PC = استُلم مفتاح OpenSubtitles من هاتفك أو الكمبيوتر
|
||||
A file arrived from your phone or PC = وصل ملف من هاتفك أو الكمبيوتر
|
||||
File = ملف
|
||||
%s file = ملف %s
|
||||
OpenSubtitles refused this API key = رفض OpenSubtitles مفتاح API هذا
|
||||
New playlist = قائمة تشغيل جديدة
|
||||
Made in VLC = أُنشئت في VLC
|
||||
Where = المكان
|
||||
1 item = عنصر واحد
|
||||
Removed from %s = أُزيل من %s
|
||||
This playlist is empty = قائمة التشغيل هذه فارغة
|
||||
Create = إنشاء
|
||||
Play all = تشغيل الكل
|
||||
Remove = إزالة
|
||||
Move = نقل
|
||||
Add to playlist = إضافة إلى قائمة تشغيل
|
||||
Rename playlist = إعادة تسمية قائمة التشغيل
|
||||
Delete playlist = حذف قائمة التشغيل
|
||||
Name the playlist = سمِّ قائمة التشغيل
|
||||
%s goes in first = %s يُضاف أولاً
|
||||
Type a name = اكتب اسمًا
|
||||
Made %s = أُنشئت %s
|
||||
Added to %s = أُضيف إلى %s
|
||||
Already in %s = موجود في %s بالفعل
|
||||
Save = حفظ
|
||||
%d playlists = %d قوائم تشغيل
|
||||
Saved in the media folder's Playlists, as a file VLC on a computer opens too = تُحفظ في مجلد Playlists داخل مجلد الوسائط، كملف يفتحه VLC على الكمبيوتر أيضًا
|
||||
A playlist with this name is already there, or the disk can't be written = توجد قائمة تشغيل بهذا الاسم، أو لا يمكن الكتابة على القرص
|
||||
Then add videos and songs from anywhere with the Options menu = ثم أضف الفيديوهات والأغاني من أي مكان عبر قائمة الخيارات
|
||||
Open the Options menu on a video or song and choose Add to playlist = افتح قائمة الخيارات على فيديو أو أغنية واختر إضافة إلى قائمة تشغيل
|
||||
stereo = ستيريو
|
||||
Chapter %d = الفصل %d
|
||||
Audio %d = الصوت %d
|
||||
Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. Subtitles: OpenSubtitles.com. = الخطوط: Selawik وInter وNoto وDejaVu (OFL). قوائم القنوات: iptv-org. الراديو: radio-browser.info. الترجمات: OpenSubtitles.com.
|
||||
15 min = 15 دقيقة
|
||||
30 min = 30 دقيقة
|
||||
45 min = 45 دقيقة
|
||||
60 min = 60 دقيقة
|
||||
90 min = 90 دقيقة
|
||||
Sign in to the share = تسجيل الدخول إلى المشاركة
|
||||
%s needs a user name and password. = يطلب %s اسم مستخدم وكلمة مرور.
|
||||
A Windows PC takes its own account; a NAS, the user you made on it. = كمبيوتر ويندوز يقبل حسابه نفسه، وجهاز NAS يقبل المستخدم الذي أنشأته عليه.
|
||||
Track %d = المسار %d
|
||||
Track = مسار
|
||||
+49
-10
@@ -146,13 +146,11 @@ About VLC = Über VLC
|
||||
PS5 port = PS5-Portierung
|
||||
VideoLAN and the VLC contributors = VideoLAN und die VLC-Mitwirkenden
|
||||
Built with = Erstellt mit
|
||||
GPLv2+. An unofficial port, not affiliated with VideoLAN or Sony. = GPLv2+. Eine inoffizielle Portierung, nicht mit VideoLAN oder Sony verbunden.
|
||||
GPL-3.0. An unofficial port, not affiliated with VideoLAN or Sony. = GPL-3.0. Eine inoffizielle Portierung, nicht mit VideoLAN oder Sony verbunden.
|
||||
Resume = Fortsetzen
|
||||
Audio, subtitles and more = Audio, Untertitel und mehr
|
||||
Phone remote = Handy-Fernbedienung
|
||||
Settings = Einstellungen
|
||||
Stop playback = Wiedergabe beenden
|
||||
Send files from a phone = Dateien vom Handy senden
|
||||
Rescan media = Medien neu einlesen
|
||||
Options = Optionen
|
||||
Found %zu files = %zu Dateien gefunden
|
||||
@@ -188,7 +186,7 @@ Writes VLC's warnings and stats to vlc-ps5.log, for bug reports = Schreibt VLCs
|
||||
What VLC can read on this console = Was VLC auf dieser Konsole lesen kann
|
||||
Starts VLC again: it then sees drives plugged in since (when sandboxed) = Startet VLC neu: Dann sieht es seitdem angeschlossene Laufwerke (in der Sandbox)
|
||||
Version, credits and licences = Version, Mitwirkende und Lizenzen
|
||||
The page that sends files and works as a remote, on your home network = Die Seite, die Dateien sendet und als Fernbedienung dient, in deinem Heimnetz
|
||||
The page that sends files and takes your OpenSubtitles key, on your home network = Die Seite, die Dateien sendet und deinen OpenSubtitles-Schlüssel annimmt, in deinem Heimnetz
|
||||
Classic: panels and folders like a desktop player. Modern: black, VLC 4 style. VLC restarts = Klassisch: Bereiche und Ordner wie ein Desktop-Player. Modern: schwarz, im Stil von VLC 4. VLC startet neu
|
||||
Your own free OpenSubtitles API key (and account), here or from the phone page = Dein eigener kostenloser OpenSubtitles-API-Schlüssel (und Konto), hier oder über die Handy-Seite
|
||||
The language of VLC's menus = Die Sprache der VLC-Menüs
|
||||
@@ -228,16 +226,13 @@ Back right = Hinten rechts
|
||||
Each speaker plays a tone in turn. If one comes from the wrong place, tell us which. = Jeder Lautsprecher spielt nacheinander einen Ton. Kommt einer von der falschen Stelle, sag uns welcher.
|
||||
Stop going round = Runde stoppen
|
||||
Go round = Runde starten
|
||||
Send files from your phone = Dateien vom Handy senden
|
||||
Phone access is off. Turn it on in Settings > System. = Der Handy-Zugriff ist aus. Schalte ihn unter Einstellungen > System ein.
|
||||
The PS5 isn't connected to a network. Connect it, then come back. = Die PS5 ist mit keinem Netzwerk verbunden. Verbinde sie und komm dann zurück.
|
||||
On a phone or computer on the same Wi-Fi, open this address in the browser: = Öffne auf einem Handy oder Computer im selben WLAN diese Adresse im Browser:
|
||||
Send videos, music and subtitles to the PS5 = Sende Videos, Musik und Untertitel an die PS5
|
||||
Use it as a remote while something plays = Nutze es während der Wiedergabe als Fernbedienung
|
||||
Add your OpenSubtitles API key = Deinen OpenSubtitles-API-Schlüssel hinzufügen
|
||||
Receiving %s = Empfange %s
|
||||
Volume %d%% = Lautstärke %d %%
|
||||
OpenSubtitles key received from your phone = OpenSubtitles-Schlüssel vom Handy empfangen
|
||||
A file arrived from your phone = Eine Datei ist vom Handy angekommen
|
||||
VLC media folder = VLC-Medienordner
|
||||
USB drive %d = USB-Laufwerk %d
|
||||
USB drive (%s) = USB-Laufwerk (%s)
|
||||
@@ -387,12 +382,10 @@ The API key works = Der API-Schlüssel funktioniert
|
||||
Checking the key = Schlüsselprüfung
|
||||
The server didn't answer. = Der Server hat nicht geantwortet.
|
||||
Couldn't open this place. = Dieser Ort konnte nicht geöffnet werden.
|
||||
File = Datei
|
||||
Folder = Ordner
|
||||
Bitrate = Bitrate
|
||||
Container = Container
|
||||
Video = Video
|
||||
Track = Spur
|
||||
White = Weiß
|
||||
Yellow = Gelb
|
||||
Cyan = Cyan
|
||||
@@ -512,3 +505,49 @@ Skip = Überspringen
|
||||
Start = Start
|
||||
Run setup again = Einrichtung wiederholen
|
||||
Language, look and playback, chosen again = Sprache, Aussehen und Wiedergabe neu wählen
|
||||
Send files from a phone or PC = Dateien vom Handy oder PC senden
|
||||
Send files from your phone or PC = Dateien vom Handy oder PC senden
|
||||
OpenSubtitles key received from your phone or PC = OpenSubtitles-Schlüssel vom Handy oder PC empfangen
|
||||
A file arrived from your phone or PC = Eine Datei ist vom Handy oder PC angekommen
|
||||
File = Datei
|
||||
%s file = %s-Datei
|
||||
OpenSubtitles refused this API key = OpenSubtitles hat diesen API-Schlüssel abgelehnt
|
||||
New playlist = Neue Playlist
|
||||
Made in VLC = In VLC erstellt
|
||||
Where = Ort
|
||||
1 item = 1 Element
|
||||
Removed from %s = Aus %s entfernt
|
||||
This playlist is empty = Diese Playlist ist leer
|
||||
Create = Erstellen
|
||||
Play all = Alle abspielen
|
||||
Remove = Entfernen
|
||||
Move = Verschieben
|
||||
Add to playlist = Zur Playlist hinzufügen
|
||||
Rename playlist = Playlist umbenennen
|
||||
Delete playlist = Playlist löschen
|
||||
Name the playlist = Playlist benennen
|
||||
%s goes in first = %s kommt als Erstes hinein
|
||||
Type a name = Gib einen Namen ein
|
||||
Made %s = %s erstellt
|
||||
Added to %s = Zu %s hinzugefügt
|
||||
Already in %s = Schon in %s
|
||||
Save = Speichern
|
||||
%d playlists = %d Playlists
|
||||
Saved in the media folder's Playlists, as a file VLC on a computer opens too = Gespeichert im Ordner Playlists des Medienordners, als Datei, die VLC auf dem Computer auch öffnet
|
||||
A playlist with this name is already there, or the disk can't be written = Eine Playlist mit diesem Namen gibt es schon, oder das Laufwerk ist nicht beschreibbar
|
||||
Then add videos and songs from anywhere with the Options menu = Dann Videos und Songs von überall über das Menü Optionen hinzufügen
|
||||
Open the Options menu on a video or song and choose Add to playlist = Öffne das Menü Optionen bei einem Video oder Song und wähle Zur Playlist hinzufügen
|
||||
stereo = Stereo
|
||||
Chapter %d = Kapitel %d
|
||||
Audio %d = Audio %d
|
||||
Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. Subtitles: OpenSubtitles.com. = Schriften: Selawik, Inter, Noto, DejaVu (OFL). Senderlisten: iptv-org. Radio: radio-browser.info. Untertitel: OpenSubtitles.com.
|
||||
15 min = 15 Min.
|
||||
30 min = 30 Min.
|
||||
45 min = 45 Min.
|
||||
60 min = 60 Min.
|
||||
90 min = 90 Min.
|
||||
Sign in to the share = Bei der Freigabe anmelden
|
||||
%s needs a user name and password. = %s braucht einen Benutzernamen und ein Passwort.
|
||||
A Windows PC takes its own account; a NAS, the user you made on it. = Ein Windows-PC nimmt sein eigenes Konto, ein NAS den Benutzer, den du darauf angelegt hast.
|
||||
Track %d = Spur %d
|
||||
Track = Spur
|
||||
+49
-10
@@ -146,13 +146,11 @@ About VLC = Acerca de VLC
|
||||
PS5 port = Versión para PS5
|
||||
VideoLAN and the VLC contributors = VideoLAN y los colaboradores de VLC
|
||||
Built with = Hecho con
|
||||
GPLv2+. An unofficial port, not affiliated with VideoLAN or Sony. = GPLv2+. Una adaptación no oficial, sin relación con VideoLAN ni Sony.
|
||||
GPL-3.0. An unofficial port, not affiliated with VideoLAN or Sony. = GPL-3.0. Una adaptación no oficial, sin relación con VideoLAN ni Sony.
|
||||
Resume = Continuar
|
||||
Audio, subtitles and more = Audio, subtítulos y más
|
||||
Phone remote = Mando desde el móvil
|
||||
Settings = Ajustes
|
||||
Stop playback = Detener la reproducción
|
||||
Send files from a phone = Enviar archivos desde un móvil
|
||||
Rescan media = Volver a buscar archivos
|
||||
Options = Opciones
|
||||
Found %zu files = %zu archivos encontrados
|
||||
@@ -188,7 +186,7 @@ Writes VLC's warnings and stats to vlc-ps5.log, for bug reports = Escribe los av
|
||||
What VLC can read on this console = Lo que VLC puede leer en esta consola
|
||||
Starts VLC again: it then sees drives plugged in since (when sandboxed) = Reinicia VLC: así ve las unidades conectadas después (en modo aislado)
|
||||
Version, credits and licences = Versión, créditos y licencias
|
||||
The page that sends files and works as a remote, on your home network = La página que envía archivos y sirve de mando, en tu red doméstica
|
||||
The page that sends files and takes your OpenSubtitles key, on your home network = La página que envía archivos y recibe tu clave de OpenSubtitles, en tu red doméstica
|
||||
Classic: panels and folders like a desktop player. Modern: black, VLC 4 style. VLC restarts = Clásico: paneles y carpetas como un reproductor de escritorio. Moderno: negro, estilo VLC 4. VLC se reinicia
|
||||
Your own free OpenSubtitles API key (and account), here or from the phone page = Tu propia clave API gratuita de OpenSubtitles (y cuenta), aquí o desde la página del móvil
|
||||
The language of VLC's menus = El idioma de los menús de VLC
|
||||
@@ -228,16 +226,13 @@ Back right = Trasero derecho
|
||||
Each speaker plays a tone in turn. If one comes from the wrong place, tell us which. = Cada altavoz emite un tono por turnos. Si alguno suena desde el lugar equivocado, dinos cuál.
|
||||
Stop going round = Detener la ronda
|
||||
Go round = Hacer la ronda
|
||||
Send files from your phone = Envía archivos desde tu móvil
|
||||
Phone access is off. Turn it on in Settings > System. = El acceso desde el móvil está desactivado. Actívalo en Ajustes > Sistema.
|
||||
The PS5 isn't connected to a network. Connect it, then come back. = La PS5 no está conectada a una red. Conéctala y vuelve.
|
||||
On a phone or computer on the same Wi-Fi, open this address in the browser: = En un móvil u ordenador de la misma Wi-Fi, abre esta dirección en el navegador:
|
||||
Send videos, music and subtitles to the PS5 = Envía vídeos, música y subtítulos a la PS5
|
||||
Use it as a remote while something plays = Úsala como mando mientras algo se reproduce
|
||||
Add your OpenSubtitles API key = Añade tu clave API de OpenSubtitles
|
||||
Receiving %s = Recibiendo %s
|
||||
Volume %d%% = Volumen %d %%
|
||||
OpenSubtitles key received from your phone = Clave de OpenSubtitles recibida desde tu móvil
|
||||
A file arrived from your phone = Ha llegado un archivo desde tu móvil
|
||||
VLC media folder = Carpeta multimedia de VLC
|
||||
USB drive %d = Unidad USB %d
|
||||
USB drive (%s) = Unidad USB (%s)
|
||||
@@ -387,12 +382,10 @@ The API key works = La clave API funciona
|
||||
Checking the key = La comprobación de la clave
|
||||
The server didn't answer. = El servidor no respondió.
|
||||
Couldn't open this place. = No se pudo abrir esta ubicación.
|
||||
File = Archivo
|
||||
Folder = Carpeta
|
||||
Bitrate = Tasa de bits
|
||||
Container = Contenedor
|
||||
Video = Vídeo
|
||||
Track = Pista
|
||||
White = Blanco
|
||||
Yellow = Amarillo
|
||||
Cyan = Cian
|
||||
@@ -512,3 +505,49 @@ Skip = Omitir
|
||||
Start = Empezar
|
||||
Run setup again = Repetir la configuración
|
||||
Language, look and playback, chosen again = Idioma, aspecto y reproducción, de nuevo
|
||||
Send files from a phone or PC = Enviar archivos desde un móvil o PC
|
||||
Send files from your phone or PC = Envía archivos desde tu móvil o PC
|
||||
OpenSubtitles key received from your phone or PC = Clave de OpenSubtitles recibida desde tu móvil o PC
|
||||
A file arrived from your phone or PC = Ha llegado un archivo desde tu móvil o PC
|
||||
File = Archivo
|
||||
%s file = Archivo %s
|
||||
OpenSubtitles refused this API key = OpenSubtitles rechazó esta clave API
|
||||
New playlist = Nueva lista de reproducción
|
||||
Made in VLC = Creada en VLC
|
||||
Where = Ubicación
|
||||
1 item = 1 elemento
|
||||
Removed from %s = Quitado de %s
|
||||
This playlist is empty = Esta lista de reproducción está vacía
|
||||
Create = Crear
|
||||
Play all = Reproducir todo
|
||||
Remove = Quitar
|
||||
Move = Mover
|
||||
Add to playlist = Añadir a una lista de reproducción
|
||||
Rename playlist = Renombrar lista de reproducción
|
||||
Delete playlist = Eliminar lista de reproducción
|
||||
Name the playlist = Ponle nombre a la lista de reproducción
|
||||
%s goes in first = %s entra primero
|
||||
Type a name = Escribe un nombre
|
||||
Made %s = Creada %s
|
||||
Added to %s = Añadido a %s
|
||||
Already in %s = Ya está en %s
|
||||
Save = Guardar
|
||||
%d playlists = %d listas de reproducción
|
||||
Saved in the media folder's Playlists, as a file VLC on a computer opens too = Se guarda en la carpeta Playlists de la carpeta de medios, como un archivo que VLC en un ordenador también abre
|
||||
A playlist with this name is already there, or the disk can't be written = Ya hay una lista de reproducción con este nombre, o no se puede escribir en el disco
|
||||
Then add videos and songs from anywhere with the Options menu = Luego añade vídeos y canciones desde cualquier sitio con el menú Opciones
|
||||
Open the Options menu on a video or song and choose Add to playlist = Abre el menú Opciones en un vídeo o una canción y elige Añadir a una lista de reproducción
|
||||
stereo = estéreo
|
||||
Chapter %d = Capítulo %d
|
||||
Audio %d = Audio %d
|
||||
Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. Subtitles: OpenSubtitles.com. = Fuentes: Selawik, Inter, Noto, DejaVu (OFL). Listas de TV: iptv-org. Radio: radio-browser.info. Subtítulos: OpenSubtitles.com.
|
||||
15 min = 15 min
|
||||
30 min = 30 min
|
||||
45 min = 45 min
|
||||
60 min = 60 min
|
||||
90 min = 90 min
|
||||
Sign in to the share = Iniciar sesión en la carpeta compartida
|
||||
%s needs a user name and password. = %s necesita un usuario y una contraseña.
|
||||
A Windows PC takes its own account; a NAS, the user you made on it. = Un PC con Windows usa su propia cuenta; un NAS, el usuario que creaste en él.
|
||||
Track %d = Pista %d
|
||||
Track = Pista
|
||||
+49
-15
@@ -16,11 +16,6 @@ Stretch = Étirer
|
||||
21:9 (cinema) = 21:9 (cinéma)
|
||||
Zoom 125% = Zoom 125 %
|
||||
Off = Désactivé
|
||||
15 min = 15 min
|
||||
30 min = 30 min
|
||||
45 min = 45 min
|
||||
60 min = 60 min
|
||||
90 min = 90 min
|
||||
End of this file = Fin de ce fichier
|
||||
Continue watching = Reprendre la lecture
|
||||
Recently added = Ajoutés récemment
|
||||
@@ -151,13 +146,11 @@ About VLC = À propos de VLC
|
||||
PS5 port = Portage PS5
|
||||
VideoLAN and the VLC contributors = VideoLAN et les contributeurs de VLC
|
||||
Built with = Conçu avec
|
||||
GPLv2+. An unofficial port, not affiliated with VideoLAN or Sony. = GPLv2+. Un portage non officiel, sans lien avec VideoLAN ni Sony.
|
||||
GPL-3.0. An unofficial port, not affiliated with VideoLAN or Sony. = GPL-3.0. Un portage non officiel, sans lien avec VideoLAN ni Sony.
|
||||
Resume = Reprendre
|
||||
Audio, subtitles and more = Audio, sous-titres et plus
|
||||
Phone remote = Télécommande téléphone
|
||||
Settings = Paramètres
|
||||
Stop playback = Arrêter la lecture
|
||||
Send files from a phone = Envoyer des fichiers depuis un téléphone
|
||||
Rescan media = Réanalyser les médias
|
||||
Options = Options
|
||||
Found %zu files = %zu fichiers trouvés
|
||||
@@ -194,7 +187,7 @@ Writes VLC's warnings and stats to vlc-ps5.log, for bug reports = Écrit les ave
|
||||
What VLC can read on this console = Ce que VLC peut lire sur cette console
|
||||
Starts VLC again: it then sees drives plugged in since (when sandboxed) = Redémarre VLC : il voit alors les lecteurs branchés depuis (en mode isolé)
|
||||
Version, credits and licences = Version, crédits et licences
|
||||
The page that sends files and works as a remote, on your home network = La page qui envoie des fichiers et sert de télécommande, sur votre réseau domestique
|
||||
The page that sends files and takes your OpenSubtitles key, on your home network = La page qui envoie des fichiers et reçoit votre clé OpenSubtitles, sur votre réseau domestique
|
||||
Classic: panels and folders like a desktop player. Modern: black, VLC 4 style. VLC restarts = Classique : panneaux et dossiers comme un lecteur de bureau. Moderne : noir, style VLC 4. VLC redémarre
|
||||
Your own free OpenSubtitles API key (and account), here or from the phone page = Votre propre clé API OpenSubtitles gratuite (et compte), ici ou depuis la page du téléphone
|
||||
The language of VLC's menus = La langue des menus de VLC
|
||||
@@ -234,16 +227,13 @@ Back right = Arrière droite
|
||||
Each speaker plays a tone in turn. If one comes from the wrong place, tell us which. = Chaque enceinte joue un son à tour de rôle. Si l'un vient du mauvais endroit, dites-nous lequel.
|
||||
Stop going round = Arrêter le tour
|
||||
Go round = Faire le tour
|
||||
Send files from your phone = Envoyer des fichiers depuis votre téléphone
|
||||
Phone access is off. Turn it on in Settings > System. = L'accès téléphone est désactivé. Activez-le dans Paramètres > Système.
|
||||
The PS5 isn't connected to a network. Connect it, then come back. = La PS5 n'est pas connectée à un réseau. Connectez-la, puis revenez.
|
||||
On a phone or computer on the same Wi-Fi, open this address in the browser: = Sur un téléphone ou un ordinateur du même Wi-Fi, ouvrez cette adresse dans le navigateur :
|
||||
Send videos, music and subtitles to the PS5 = Envoyez vidéos, musique et sous-titres à la PS5
|
||||
Use it as a remote while something plays = Utilisez-la comme télécommande pendant la lecture
|
||||
Add your OpenSubtitles API key = Ajoutez votre clé API OpenSubtitles
|
||||
Receiving %s = Réception de %s
|
||||
Volume %d%% = Volume %d %%
|
||||
OpenSubtitles key received from your phone = Clé OpenSubtitles reçue depuis votre téléphone
|
||||
A file arrived from your phone = Un fichier est arrivé de votre téléphone
|
||||
VLC media folder = Dossier multimédia VLC
|
||||
USB drive %d = Clé USB %d
|
||||
USB drive (%s) = Clé USB (%s)
|
||||
@@ -393,12 +383,10 @@ The API key works = La clé API fonctionne
|
||||
Checking the key = La vérification de la clé
|
||||
The server didn't answer. = Le serveur n'a pas répondu.
|
||||
Couldn't open this place. = Impossible d'ouvrir cet emplacement.
|
||||
File = Fichier
|
||||
Folder = Dossier
|
||||
Bitrate = Débit
|
||||
Container = Conteneur
|
||||
Video = Vidéo
|
||||
Track = Piste
|
||||
White = Blanc
|
||||
Yellow = Jaune
|
||||
Cyan = Cyan
|
||||
@@ -518,3 +506,49 @@ Skip = Passer
|
||||
Start = Commencer
|
||||
Run setup again = Relancer la configuration
|
||||
Language, look and playback, chosen again = Langue, apparence et lecture, à nouveau
|
||||
Send files from a phone or PC = Envoyer des fichiers depuis un téléphone ou un PC
|
||||
Send files from your phone or PC = Envoyer des fichiers depuis votre téléphone ou PC
|
||||
OpenSubtitles key received from your phone or PC = Clé OpenSubtitles reçue depuis votre téléphone ou PC
|
||||
A file arrived from your phone or PC = Un fichier est arrivé de votre téléphone ou PC
|
||||
File = Fichier
|
||||
%s file = Fichier %s
|
||||
OpenSubtitles refused this API key = OpenSubtitles a refusé cette clé API
|
||||
New playlist = Nouvelle liste de lecture
|
||||
Made in VLC = Créée dans VLC
|
||||
Where = Emplacement
|
||||
1 item = 1 élément
|
||||
Removed from %s = Retiré de %s
|
||||
This playlist is empty = Cette liste de lecture est vide
|
||||
Create = Créer
|
||||
Play all = Tout lire
|
||||
Remove = Retirer
|
||||
Move = Déplacer
|
||||
Add to playlist = Ajouter à une liste de lecture
|
||||
Rename playlist = Renommer la liste de lecture
|
||||
Delete playlist = Supprimer la liste de lecture
|
||||
Name the playlist = Nommez la liste de lecture
|
||||
%s goes in first = %s y entre en premier
|
||||
Type a name = Saisissez un nom
|
||||
Made %s = %s créée
|
||||
Added to %s = Ajouté à %s
|
||||
Already in %s = Déjà dans %s
|
||||
Save = Enregistrer
|
||||
%d playlists = %d listes de lecture
|
||||
Saved in the media folder's Playlists, as a file VLC on a computer opens too = Enregistrée dans le dossier Playlists du dossier des médias, en fichier que VLC sur ordinateur ouvre aussi
|
||||
A playlist with this name is already there, or the disk can't be written = Une liste de lecture porte déjà ce nom, ou le disque n'est pas accessible en écriture
|
||||
Then add videos and songs from anywhere with the Options menu = Ajoutez ensuite vidéos et morceaux de partout avec le menu Options
|
||||
Open the Options menu on a video or song and choose Add to playlist = Ouvrez le menu Options sur une vidéo ou un morceau et choisissez Ajouter à une liste de lecture
|
||||
stereo = stéréo
|
||||
Chapter %d = Chapitre %d
|
||||
Audio %d = Audio %d
|
||||
Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. Subtitles: OpenSubtitles.com. = Polices : Selawik, Inter, Noto, DejaVu (OFL). Listes TV : iptv-org. Radio : radio-browser.info. Sous-titres : OpenSubtitles.com.
|
||||
15 min = 15 min
|
||||
30 min = 30 min
|
||||
45 min = 45 min
|
||||
60 min = 60 min
|
||||
90 min = 90 min
|
||||
Sign in to the share = Se connecter au partage
|
||||
%s needs a user name and password. = %s demande un nom d'utilisateur et un mot de passe.
|
||||
A Windows PC takes its own account; a NAS, the user you made on it. = Un PC Windows prend son propre compte ; un NAS, l'utilisateur que vous y avez créé.
|
||||
Track %d = Piste %d
|
||||
Track = Piste
|
||||
+49
-10
@@ -146,13 +146,11 @@ About VLC = Tentang VLC
|
||||
PS5 port = Port PS5
|
||||
VideoLAN and the VLC contributors = VideoLAN dan para kontributor VLC
|
||||
Built with = Dibuat dengan
|
||||
GPLv2+. An unofficial port, not affiliated with VideoLAN or Sony. = GPLv2+. Port tidak resmi, tidak berafiliasi dengan VideoLAN atau Sony.
|
||||
GPL-3.0. An unofficial port, not affiliated with VideoLAN or Sony. = GPL-3.0. Port tidak resmi, tidak berafiliasi dengan VideoLAN atau Sony.
|
||||
Resume = Lanjutkan
|
||||
Audio, subtitles and more = Audio, subtitle, dan lainnya
|
||||
Phone remote = Remote ponsel
|
||||
Settings = Pengaturan
|
||||
Stop playback = Hentikan pemutaran
|
||||
Send files from a phone = Kirim file dari ponsel
|
||||
Rescan media = Pindai ulang media
|
||||
Options = Opsi
|
||||
Found %zu files = %zu file ditemukan
|
||||
@@ -188,7 +186,7 @@ Writes VLC's warnings and stats to vlc-ps5.log, for bug reports = Menulis pering
|
||||
What VLC can read on this console = Apa yang dapat dibaca VLC di konsol ini
|
||||
Starts VLC again: it then sees drives plugged in since (when sandboxed) = Memulai ulang VLC: lalu melihat drive yang dicolokkan setelahnya (saat dibatasi)
|
||||
Version, credits and licences = Versi, kredit, dan lisensi
|
||||
The page that sends files and works as a remote, on your home network = Halaman untuk mengirim file dan menjadi remote, di jaringan rumah Anda
|
||||
The page that sends files and takes your OpenSubtitles key, on your home network = Halaman untuk mengirim file dan memasukkan kunci OpenSubtitles, di jaringan rumah Anda
|
||||
Classic: panels and folders like a desktop player. Modern: black, VLC 4 style. VLC restarts = Klasik: panel dan folder seperti pemutar desktop. Modern: hitam, gaya VLC 4. VLC dimulai ulang
|
||||
Your own free OpenSubtitles API key (and account), here or from the phone page = Kunci API OpenSubtitles gratis milik Anda (dan akun), di sini atau dari halaman ponsel
|
||||
The language of VLC's menus = Bahasa menu VLC
|
||||
@@ -228,16 +226,13 @@ Back right = Belakang kanan
|
||||
Each speaker plays a tone in turn. If one comes from the wrong place, tell us which. = Setiap speaker memutar nada bergantian. Jika ada yang terdengar dari tempat yang salah, beri tahu kami yang mana.
|
||||
Stop going round = Hentikan putaran
|
||||
Go round = Mulai putaran
|
||||
Send files from your phone = Kirim file dari ponsel Anda
|
||||
Phone access is off. Turn it on in Settings > System. = Akses ponsel mati. Nyalakan di Pengaturan > Sistem.
|
||||
The PS5 isn't connected to a network. Connect it, then come back. = PS5 tidak terhubung ke jaringan. Hubungkan, lalu kembali.
|
||||
On a phone or computer on the same Wi-Fi, open this address in the browser: = Di ponsel atau komputer pada Wi-Fi yang sama, buka alamat ini di browser:
|
||||
Send videos, music and subtitles to the PS5 = Kirim video, musik, dan subtitle ke PS5
|
||||
Use it as a remote while something plays = Gunakan sebagai remote saat ada yang diputar
|
||||
Add your OpenSubtitles API key = Tambahkan kunci API OpenSubtitles Anda
|
||||
Receiving %s = Menerima %s
|
||||
Volume %d%% = Volume %d%%
|
||||
OpenSubtitles key received from your phone = Kunci OpenSubtitles diterima dari ponsel Anda
|
||||
A file arrived from your phone = File tiba dari ponsel Anda
|
||||
VLC media folder = Folder media VLC
|
||||
USB drive %d = Drive USB %d
|
||||
USB drive (%s) = Drive USB (%s)
|
||||
@@ -387,12 +382,10 @@ The API key works = Kunci API berfungsi
|
||||
Checking the key = Pemeriksaan kunci
|
||||
The server didn't answer. = Server tidak menjawab.
|
||||
Couldn't open this place. = Tidak dapat membuka lokasi ini.
|
||||
File = File
|
||||
Folder = Folder
|
||||
Bitrate = Bitrate
|
||||
Container = Kontainer
|
||||
Video = Video
|
||||
Track = Trek
|
||||
White = Putih
|
||||
Yellow = Kuning
|
||||
Cyan = Sian
|
||||
@@ -512,3 +505,49 @@ Skip = Lewati
|
||||
Start = Mulai
|
||||
Run setup again = Jalankan penyiapan lagi
|
||||
Language, look and playback, chosen again = Bahasa, tampilan, dan pemutaran dipilih ulang
|
||||
Send files from a phone or PC = Kirim file dari ponsel atau PC
|
||||
Send files from your phone or PC = Kirim file dari ponsel atau PC Anda
|
||||
OpenSubtitles key received from your phone or PC = Kunci OpenSubtitles diterima dari ponsel atau PC Anda
|
||||
A file arrived from your phone or PC = File tiba dari ponsel atau PC Anda
|
||||
File = File
|
||||
%s file = File %s
|
||||
OpenSubtitles refused this API key = OpenSubtitles menolak kunci API ini
|
||||
New playlist = Daftar putar baru
|
||||
Made in VLC = Dibuat di VLC
|
||||
Where = Lokasi
|
||||
1 item = 1 item
|
||||
Removed from %s = Dihapus dari %s
|
||||
This playlist is empty = Daftar putar ini kosong
|
||||
Create = Buat
|
||||
Play all = Putar semua
|
||||
Remove = Hapus
|
||||
Move = Pindahkan
|
||||
Add to playlist = Tambahkan ke daftar putar
|
||||
Rename playlist = Ganti nama daftar putar
|
||||
Delete playlist = Hapus daftar putar
|
||||
Name the playlist = Beri nama daftar putar
|
||||
%s goes in first = %s masuk lebih dulu
|
||||
Type a name = Ketik nama
|
||||
Made %s = %s dibuat
|
||||
Added to %s = Ditambahkan ke %s
|
||||
Already in %s = Sudah ada di %s
|
||||
Save = Simpan
|
||||
%d playlists = %d daftar putar
|
||||
Saved in the media folder's Playlists, as a file VLC on a computer opens too = Disimpan di folder Playlists dalam folder media, sebagai file yang juga bisa dibuka VLC di komputer
|
||||
A playlist with this name is already there, or the disk can't be written = Sudah ada daftar putar dengan nama ini, atau disk tidak bisa ditulisi
|
||||
Then add videos and songs from anywhere with the Options menu = Lalu tambahkan video dan lagu dari mana saja lewat menu Opsi
|
||||
Open the Options menu on a video or song and choose Add to playlist = Buka menu Opsi pada video atau lagu lalu pilih Tambahkan ke daftar putar
|
||||
stereo = stereo
|
||||
Chapter %d = Bab %d
|
||||
Audio %d = Audio %d
|
||||
Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. Subtitles: OpenSubtitles.com. = Font: Selawik, Inter, Noto, DejaVu (OFL). Daftar TV: iptv-org. Radio: radio-browser.info. Subtitle: OpenSubtitles.com.
|
||||
15 min = 15 mnt
|
||||
30 min = 30 mnt
|
||||
45 min = 45 mnt
|
||||
60 min = 60 mnt
|
||||
90 min = 90 mnt
|
||||
Sign in to the share = Masuk ke folder berbagi
|
||||
%s needs a user name and password. = %s memerlukan nama pengguna dan kata sandi.
|
||||
A Windows PC takes its own account; a NAS, the user you made on it. = PC Windows memakai akunnya sendiri; NAS memakai pengguna yang Anda buat di sana.
|
||||
Track %d = Trek %d
|
||||
Track = Trek
|
||||
+49
-10
@@ -146,13 +146,11 @@ About VLC = Informazioni su VLC
|
||||
PS5 port = Versione PS5
|
||||
VideoLAN and the VLC contributors = VideoLAN e i collaboratori di VLC
|
||||
Built with = Realizzato con
|
||||
GPLv2+. An unofficial port, not affiliated with VideoLAN or Sony. = GPLv2+. Una versione non ufficiale, non affiliata a VideoLAN o Sony.
|
||||
GPL-3.0. An unofficial port, not affiliated with VideoLAN or Sony. = GPL-3.0. Una versione non ufficiale, non affiliata a VideoLAN o Sony.
|
||||
Resume = Riprendi
|
||||
Audio, subtitles and more = Audio, sottotitoli e altro
|
||||
Phone remote = Telecomando dal telefono
|
||||
Settings = Impostazioni
|
||||
Stop playback = Interrompi la riproduzione
|
||||
Send files from a phone = Invia file da un telefono
|
||||
Rescan media = Ricerca di nuovo i contenuti
|
||||
Options = Opzioni
|
||||
Found %zu files = %zu file trovati
|
||||
@@ -188,7 +186,7 @@ Writes VLC's warnings and stats to vlc-ps5.log, for bug reports = Scrive avvisi
|
||||
What VLC can read on this console = Cosa può leggere VLC su questa console
|
||||
Starts VLC again: it then sees drives plugged in since (when sandboxed) = Riavvia VLC: così vede le unità collegate nel frattempo (in sandbox)
|
||||
Version, credits and licences = Versione, riconoscimenti e licenze
|
||||
The page that sends files and works as a remote, on your home network = La pagina che invia file e fa da telecomando, sulla rete di casa
|
||||
The page that sends files and takes your OpenSubtitles key, on your home network = La pagina che invia file e riceve la tua chiave OpenSubtitles, sulla rete di casa
|
||||
Classic: panels and folders like a desktop player. Modern: black, VLC 4 style. VLC restarts = Classico: pannelli e cartelle come un lettore desktop. Moderno: nero, stile VLC 4. VLC si riavvia
|
||||
Your own free OpenSubtitles API key (and account), here or from the phone page = La tua chiave API gratuita di OpenSubtitles (e account), qui o dalla pagina del telefono
|
||||
The language of VLC's menus = La lingua dei menu di VLC
|
||||
@@ -228,16 +226,13 @@ Back right = Posteriore destro
|
||||
Each speaker plays a tone in turn. If one comes from the wrong place, tell us which. = Ogni altoparlante emette un tono a turno. Se uno arriva dal punto sbagliato, dicci quale.
|
||||
Stop going round = Ferma il giro
|
||||
Go round = Fai il giro
|
||||
Send files from your phone = Invia file dal tuo telefono
|
||||
Phone access is off. Turn it on in Settings > System. = L'accesso dal telefono è disattivato. Attivalo in Impostazioni > Sistema.
|
||||
The PS5 isn't connected to a network. Connect it, then come back. = La PS5 non è connessa a una rete. Connettila, poi torna qui.
|
||||
On a phone or computer on the same Wi-Fi, open this address in the browser: = Su un telefono o computer della stessa Wi-Fi, apri questo indirizzo nel browser:
|
||||
Send videos, music and subtitles to the PS5 = Invia video, musica e sottotitoli alla PS5
|
||||
Use it as a remote while something plays = Usala come telecomando durante la riproduzione
|
||||
Add your OpenSubtitles API key = Aggiungi la tua chiave API OpenSubtitles
|
||||
Receiving %s = Ricezione di %s
|
||||
Volume %d%% = Volume %d%%
|
||||
OpenSubtitles key received from your phone = Chiave OpenSubtitles ricevuta dal telefono
|
||||
A file arrived from your phone = È arrivato un file dal telefono
|
||||
VLC media folder = Cartella multimediale di VLC
|
||||
USB drive %d = Unità USB %d
|
||||
USB drive (%s) = Unità USB (%s)
|
||||
@@ -387,12 +382,10 @@ The API key works = La chiave API funziona
|
||||
Checking the key = La verifica della chiave
|
||||
The server didn't answer. = Il server non ha risposto.
|
||||
Couldn't open this place. = Impossibile aprire questa posizione.
|
||||
File = File
|
||||
Folder = Cartella
|
||||
Bitrate = Bitrate
|
||||
Container = Contenitore
|
||||
Video = Video
|
||||
Track = Traccia
|
||||
White = Bianco
|
||||
Yellow = Giallo
|
||||
Cyan = Ciano
|
||||
@@ -512,3 +505,49 @@ Skip = Salta
|
||||
Start = Inizia
|
||||
Run setup again = Ripeti la configurazione
|
||||
Language, look and playback, chosen again = Lingua, aspetto e riproduzione, di nuovo
|
||||
Send files from a phone or PC = Invia file da un telefono o PC
|
||||
Send files from your phone or PC = Invia file dal tuo telefono o PC
|
||||
OpenSubtitles key received from your phone or PC = Chiave OpenSubtitles ricevuta dal telefono o PC
|
||||
A file arrived from your phone or PC = È arrivato un file dal telefono o PC
|
||||
File = File
|
||||
%s file = File %s
|
||||
OpenSubtitles refused this API key = OpenSubtitles ha rifiutato questa chiave API
|
||||
New playlist = Nuova playlist
|
||||
Made in VLC = Creata in VLC
|
||||
Where = Posizione
|
||||
1 item = 1 elemento
|
||||
Removed from %s = Rimosso da %s
|
||||
This playlist is empty = Questa playlist è vuota
|
||||
Create = Crea
|
||||
Play all = Riproduci tutto
|
||||
Remove = Rimuovi
|
||||
Move = Sposta
|
||||
Add to playlist = Aggiungi a una playlist
|
||||
Rename playlist = Rinomina playlist
|
||||
Delete playlist = Elimina playlist
|
||||
Name the playlist = Dai un nome alla playlist
|
||||
%s goes in first = %s entra per primo
|
||||
Type a name = Scrivi un nome
|
||||
Made %s = %s creata
|
||||
Added to %s = Aggiunto a %s
|
||||
Already in %s = Già in %s
|
||||
Save = Salva
|
||||
%d playlists = %d playlist
|
||||
Saved in the media folder's Playlists, as a file VLC on a computer opens too = Salvata nella cartella Playlists della cartella dei media, come file che anche VLC sul computer apre
|
||||
A playlist with this name is already there, or the disk can't be written = Esiste già una playlist con questo nome, oppure il disco non è scrivibile
|
||||
Then add videos and songs from anywhere with the Options menu = Poi aggiungi video e brani da qualsiasi punto con il menu Opzioni
|
||||
Open the Options menu on a video or song and choose Add to playlist = Apri il menu Opzioni su un video o un brano e scegli Aggiungi a una playlist
|
||||
stereo = stereo
|
||||
Chapter %d = Capitolo %d
|
||||
Audio %d = Audio %d
|
||||
Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. Subtitles: OpenSubtitles.com. = Caratteri: Selawik, Inter, Noto, DejaVu (OFL). Elenchi TV: iptv-org. Radio: radio-browser.info. Sottotitoli: OpenSubtitles.com.
|
||||
15 min = 15 min
|
||||
30 min = 30 min
|
||||
45 min = 45 min
|
||||
60 min = 60 min
|
||||
90 min = 90 min
|
||||
Sign in to the share = Accedi alla condivisione
|
||||
%s needs a user name and password. = %s richiede nome utente e password.
|
||||
A Windows PC takes its own account; a NAS, the user you made on it. = Un PC Windows usa il proprio account; un NAS, l'utente che hai creato su di esso.
|
||||
Track %d = Traccia %d
|
||||
Track = Traccia
|
||||
+49
-10
@@ -146,13 +146,11 @@ About VLC = VLC について
|
||||
PS5 port = PS5 版
|
||||
VideoLAN and the VLC contributors = VideoLAN と VLC の貢献者
|
||||
Built with = 使用ソフトウェア
|
||||
GPLv2+. An unofficial port, not affiliated with VideoLAN or Sony. = GPLv2+。非公式の移植版で、VideoLAN や Sony とは関係ありません。
|
||||
GPL-3.0. An unofficial port, not affiliated with VideoLAN or Sony. = GPL-3.0。非公式の移植版で、VideoLAN や Sony とは関係ありません。
|
||||
Resume = 再開
|
||||
Audio, subtitles and more = 音声、字幕など
|
||||
Phone remote = スマホリモコン
|
||||
Settings = 設定
|
||||
Stop playback = 再生を停止
|
||||
Send files from a phone = スマホからファイルを送る
|
||||
Rescan media = メディアを再スキャン
|
||||
Options = オプション
|
||||
Found %zu files = %zu 個のファイルが見つかりました
|
||||
@@ -188,7 +186,7 @@ Writes VLC's warnings and stats to vlc-ps5.log, for bug reports = バグ報告
|
||||
What VLC can read on this console = この本体で VLC が読めるもの
|
||||
Starts VLC again: it then sees drives plugged in since (when sandboxed) = VLC を再起動:その後に接続したドライブが見えるようになります(サンドボックス時)
|
||||
Version, credits and licences = バージョン、クレジット、ライセンス
|
||||
The page that sends files and works as a remote, on your home network = ホームネットワークでファイル送信とリモコンができるページ
|
||||
The page that sends files and takes your OpenSubtitles key, on your home network = ホームネットワーク上で、ファイルを送り OpenSubtitles のキーを受け取るページ
|
||||
Classic: panels and folders like a desktop player. Modern: black, VLC 4 style. VLC restarts = クラシック:デスクトップ版のようなパネルとフォルダー。モダン:黒、VLC 4 風。VLC が再起動します
|
||||
Your own free OpenSubtitles API key (and account), here or from the phone page = あなた自身の無料 OpenSubtitles API キー(とアカウント)。ここかスマホのページから
|
||||
The language of VLC's menus = VLC のメニューの言語
|
||||
@@ -228,16 +226,13 @@ Back right = バック右
|
||||
Each speaker plays a tone in turn. If one comes from the wrong place, tell us which. = 各スピーカーが順番に音を鳴らします。違う場所から聞こえたら、どれか教えてください。
|
||||
Stop going round = 一周を止める
|
||||
Go round = 一周させる
|
||||
Send files from your phone = スマホからファイルを送る
|
||||
Phone access is off. Turn it on in Settings > System. = スマホからのアクセスはオフです。設定 > システムでオンにしてください。
|
||||
The PS5 isn't connected to a network. Connect it, then come back. = PS5 がネットワークに接続されていません。接続してから戻ってください。
|
||||
On a phone or computer on the same Wi-Fi, open this address in the browser: = 同じ Wi-Fi のスマホかパソコンのブラウザーでこのアドレスを開いてください:
|
||||
Send videos, music and subtitles to the PS5 = ビデオ、音楽、字幕を PS5 に送る
|
||||
Use it as a remote while something plays = 再生中はリモコンとして使う
|
||||
Add your OpenSubtitles API key = OpenSubtitles の API キーを追加
|
||||
Receiving %s = %s を受信中
|
||||
Volume %d%% = 音量 %d%%
|
||||
OpenSubtitles key received from your phone = スマホから OpenSubtitles のキーを受け取りました
|
||||
A file arrived from your phone = スマホからファイルが届きました
|
||||
VLC media folder = VLC メディアフォルダー
|
||||
USB drive %d = USB ドライブ %d
|
||||
USB drive (%s) = USB ドライブ(%s)
|
||||
@@ -387,12 +382,10 @@ The API key works = API キーは有効です
|
||||
Checking the key = キーの確認
|
||||
The server didn't answer. = サーバーが応答しませんでした。
|
||||
Couldn't open this place. = この場所を開けませんでした。
|
||||
File = ファイル
|
||||
Folder = フォルダー
|
||||
Bitrate = ビットレート
|
||||
Container = コンテナー
|
||||
Video = ビデオ
|
||||
Track = トラック
|
||||
White = 白
|
||||
Yellow = 黄
|
||||
Cyan = シアン
|
||||
@@ -512,3 +505,49 @@ Skip = スキップ
|
||||
Start = 開始
|
||||
Run setup again = セットアップをやり直す
|
||||
Language, look and playback, chosen again = 言語、外観、再生をもう一度選びます
|
||||
Send files from a phone or PC = スマホや PC からファイルを送る
|
||||
Send files from your phone or PC = スマホや PC からファイルを送る
|
||||
OpenSubtitles key received from your phone or PC = スマホや PC から OpenSubtitles のキーを受け取りました
|
||||
A file arrived from your phone or PC = スマホや PC からファイルが届きました
|
||||
File = ファイル
|
||||
%s file = %s ファイル
|
||||
OpenSubtitles refused this API key = OpenSubtitles がこの API キーを拒否しました
|
||||
New playlist = 新しいプレイリスト
|
||||
Made in VLC = VLC で作成
|
||||
Where = 場所
|
||||
1 item = 1 件
|
||||
Removed from %s = %s から削除しました
|
||||
This playlist is empty = このプレイリストは空です
|
||||
Create = 作成
|
||||
Play all = すべて再生
|
||||
Remove = 削除
|
||||
Move = 移動
|
||||
Add to playlist = プレイリストに追加
|
||||
Rename playlist = プレイリスト名を変更
|
||||
Delete playlist = プレイリストを削除
|
||||
Name the playlist = プレイリストの名前
|
||||
%s goes in first = 最初に %s が入ります
|
||||
Type a name = 名前を入力してください
|
||||
Made %s = %s を作成しました
|
||||
Added to %s = %s に追加しました
|
||||
Already in %s = すでに %s にあります
|
||||
Save = 保存
|
||||
%d playlists = %d 件のプレイリスト
|
||||
Saved in the media folder's Playlists, as a file VLC on a computer opens too = メディアフォルダーの Playlists に、PC の VLC でも開けるファイルとして保存されます
|
||||
A playlist with this name is already there, or the disk can't be written = 同じ名前のプレイリストがあるか、ディスクに書き込めません
|
||||
Then add videos and songs from anywhere with the Options menu = あとはどこからでもオプションメニューで動画や曲を追加できます
|
||||
Open the Options menu on a video or song and choose Add to playlist = 動画や曲でオプションメニューを開き、プレイリストに追加を選んでください
|
||||
stereo = ステレオ
|
||||
Chapter %d = チャプター %d
|
||||
Audio %d = 音声 %d
|
||||
Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. Subtitles: OpenSubtitles.com. = フォント: Selawik、Inter、Noto、DejaVu (OFL)。テレビ一覧: iptv-org。ラジオ: radio-browser.info。字幕: OpenSubtitles.com。
|
||||
15 min = 15 分
|
||||
30 min = 30 分
|
||||
45 min = 45 分
|
||||
60 min = 60 分
|
||||
90 min = 90 分
|
||||
Sign in to the share = 共有フォルダーにサインイン
|
||||
%s needs a user name and password. = %s にはユーザー名とパスワードが必要です。
|
||||
A Windows PC takes its own account; a NAS, the user you made on it. = Windows PC ならその PC のアカウント、NAS なら NAS で作ったユーザーを使います。
|
||||
Track %d = トラック %d
|
||||
Track = トラック
|
||||
+49
-10
@@ -146,13 +146,11 @@ About VLC = VLC 정보
|
||||
PS5 port = PS5 포팅
|
||||
VideoLAN and the VLC contributors = VideoLAN 및 VLC 기여자
|
||||
Built with = 사용한 소프트웨어
|
||||
GPLv2+. An unofficial port, not affiliated with VideoLAN or Sony. = GPLv2+. 비공식 포팅이며 VideoLAN 또는 Sony와 관련이 없습니다.
|
||||
GPL-3.0. An unofficial port, not affiliated with VideoLAN or Sony. = GPL-3.0. 비공식 포팅이며 VideoLAN 또는 Sony와 관련이 없습니다.
|
||||
Resume = 이어서 재생
|
||||
Audio, subtitles and more = 오디오, 자막 등
|
||||
Phone remote = 휴대폰 리모컨
|
||||
Settings = 설정
|
||||
Stop playback = 재생 중지
|
||||
Send files from a phone = 휴대폰에서 파일 보내기
|
||||
Rescan media = 미디어 다시 검색
|
||||
Options = 옵션
|
||||
Found %zu files = 파일 %zu개를 찾았습니다
|
||||
@@ -188,7 +186,7 @@ Writes VLC's warnings and stats to vlc-ps5.log, for bug reports = 버그 신고
|
||||
What VLC can read on this console = 이 콘솔에서 VLC가 읽을 수 있는 곳
|
||||
Starts VLC again: it then sees drives plugged in since (when sandboxed) = VLC를 다시 시작합니다. 그 사이 연결한 드라이브가 보입니다(샌드박스일 때)
|
||||
Version, credits and licences = 버전, 만든 사람, 라이선스
|
||||
The page that sends files and works as a remote, on your home network = 홈 네트워크에서 파일을 보내고 리모컨으로 쓰는 페이지
|
||||
The page that sends files and takes your OpenSubtitles key, on your home network = 홈 네트워크에서 파일을 보내고 OpenSubtitles 키를 받는 페이지
|
||||
Classic: panels and folders like a desktop player. Modern: black, VLC 4 style. VLC restarts = 클래식: 데스크톱 플레이어 같은 패널과 폴더. 모던: 검은색, VLC 4 스타일. VLC가 다시 시작됩니다
|
||||
Your own free OpenSubtitles API key (and account), here or from the phone page = 본인의 무료 OpenSubtitles API 키(및 계정). 여기서 또는 휴대폰 페이지에서
|
||||
The language of VLC's menus = VLC 메뉴의 언어
|
||||
@@ -228,16 +226,13 @@ Back right = 뒤쪽 오른쪽
|
||||
Each speaker plays a tone in turn. If one comes from the wrong place, tell us which. = 각 스피커가 차례로 소리를 냅니다. 잘못된 곳에서 들리면 어느 것인지 알려 주세요.
|
||||
Stop going round = 순회 멈추기
|
||||
Go round = 순회 시작
|
||||
Send files from your phone = 휴대폰에서 파일 보내기
|
||||
Phone access is off. Turn it on in Settings > System. = 휴대폰 접속이 꺼져 있습니다. 설정 > 시스템에서 켜세요.
|
||||
The PS5 isn't connected to a network. Connect it, then come back. = PS5가 네트워크에 연결되어 있지 않습니다. 연결한 뒤 다시 오세요.
|
||||
On a phone or computer on the same Wi-Fi, open this address in the browser: = 같은 Wi-Fi의 휴대폰이나 컴퓨터 브라우저에서 이 주소를 여세요:
|
||||
Send videos, music and subtitles to the PS5 = 비디오, 음악, 자막을 PS5로 보내기
|
||||
Use it as a remote while something plays = 재생 중에는 리모컨으로 사용
|
||||
Add your OpenSubtitles API key = OpenSubtitles API 키 추가
|
||||
Receiving %s = %s 받는 중
|
||||
Volume %d%% = 볼륨 %d%%
|
||||
OpenSubtitles key received from your phone = 휴대폰에서 OpenSubtitles 키를 받았습니다
|
||||
A file arrived from your phone = 휴대폰에서 파일이 도착했습니다
|
||||
VLC media folder = VLC 미디어 폴더
|
||||
USB drive %d = USB 드라이브 %d
|
||||
USB drive (%s) = USB 드라이브 (%s)
|
||||
@@ -387,12 +382,10 @@ The API key works = API 키가 작동합니다
|
||||
Checking the key = 키 확인
|
||||
The server didn't answer. = 서버가 응답하지 않았습니다.
|
||||
Couldn't open this place. = 이 위치를 열 수 없습니다.
|
||||
File = 파일
|
||||
Folder = 폴더
|
||||
Bitrate = 비트레이트
|
||||
Container = 컨테이너
|
||||
Video = 비디오
|
||||
Track = 트랙
|
||||
White = 흰색
|
||||
Yellow = 노란색
|
||||
Cyan = 청록색
|
||||
@@ -512,3 +505,49 @@ Skip = 건너뛰기
|
||||
Start = 시작
|
||||
Run setup again = 설정 다시 하기
|
||||
Language, look and playback, chosen again = 언어, 모양, 재생을 다시 고릅니다
|
||||
Send files from a phone or PC = 휴대폰이나 PC에서 파일 보내기
|
||||
Send files from your phone or PC = 휴대폰이나 PC에서 파일 보내기
|
||||
OpenSubtitles key received from your phone or PC = 휴대폰이나 PC에서 OpenSubtitles 키를 받았습니다
|
||||
A file arrived from your phone or PC = 휴대폰이나 PC에서 파일이 도착했습니다
|
||||
File = 파일
|
||||
%s file = %s 파일
|
||||
OpenSubtitles refused this API key = OpenSubtitles가 이 API 키를 거부했습니다
|
||||
New playlist = 새 재생목록
|
||||
Made in VLC = VLC에서 만듦
|
||||
Where = 위치
|
||||
1 item = 1개 항목
|
||||
Removed from %s = %s에서 제거됨
|
||||
This playlist is empty = 이 재생목록은 비어 있습니다
|
||||
Create = 만들기
|
||||
Play all = 모두 재생
|
||||
Remove = 제거
|
||||
Move = 이동
|
||||
Add to playlist = 재생목록에 추가
|
||||
Rename playlist = 재생목록 이름 바꾸기
|
||||
Delete playlist = 재생목록 삭제
|
||||
Name the playlist = 재생목록 이름
|
||||
%s goes in first = %s이(가) 먼저 들어갑니다
|
||||
Type a name = 이름을 입력하세요
|
||||
Made %s = %s을(를) 만들었습니다
|
||||
Added to %s = %s에 추가됨
|
||||
Already in %s = 이미 %s에 있음
|
||||
Save = 저장
|
||||
%d playlists = 재생목록 %d개
|
||||
Saved in the media folder's Playlists, as a file VLC on a computer opens too = 미디어 폴더의 Playlists에, PC의 VLC에서도 열리는 파일로 저장됩니다
|
||||
A playlist with this name is already there, or the disk can't be written = 같은 이름의 재생목록이 있거나 디스크에 쓸 수 없습니다
|
||||
Then add videos and songs from anywhere with the Options menu = 그다음 어디서든 옵션 메뉴로 동영상과 노래를 추가하세요
|
||||
Open the Options menu on a video or song and choose Add to playlist = 동영상이나 노래에서 옵션 메뉴를 열고 재생목록에 추가를 고르세요
|
||||
stereo = 스테레오
|
||||
Chapter %d = 챕터 %d
|
||||
Audio %d = 오디오 %d
|
||||
Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. Subtitles: OpenSubtitles.com. = 글꼴: Selawik, Inter, Noto, DejaVu (OFL). TV 목록: iptv-org. 라디오: radio-browser.info. 자막: OpenSubtitles.com.
|
||||
15 min = 15분
|
||||
30 min = 30분
|
||||
45 min = 45분
|
||||
60 min = 60분
|
||||
90 min = 90분
|
||||
Sign in to the share = 공유 폴더에 로그인
|
||||
%s needs a user name and password. = %s에 사용자 이름과 비밀번호가 필요합니다.
|
||||
A Windows PC takes its own account; a NAS, the user you made on it. = Windows PC는 그 PC의 계정을, NAS는 NAS에서 만든 사용자를 사용합니다.
|
||||
Track %d = 트랙 %d
|
||||
Track = 트랙
|
||||
+49
-10
@@ -146,13 +146,11 @@ About VLC = Over VLC
|
||||
PS5 port = PS5-versie
|
||||
VideoLAN and the VLC contributors = VideoLAN en de bijdragers aan VLC
|
||||
Built with = Gemaakt met
|
||||
GPLv2+. An unofficial port, not affiliated with VideoLAN or Sony. = GPLv2+. Een onofficiële versie, niet verbonden aan VideoLAN of Sony.
|
||||
GPL-3.0. An unofficial port, not affiliated with VideoLAN or Sony. = GPL-3.0. Een onofficiële versie, niet verbonden aan VideoLAN of Sony.
|
||||
Resume = Hervatten
|
||||
Audio, subtitles and more = Audio, ondertitels en meer
|
||||
Phone remote = Telefoonafstandsbediening
|
||||
Settings = Instellingen
|
||||
Stop playback = Afspelen stoppen
|
||||
Send files from a phone = Bestanden van een telefoon sturen
|
||||
Rescan media = Media opnieuw scannen
|
||||
Options = Opties
|
||||
Found %zu files = %zu bestanden gevonden
|
||||
@@ -188,7 +186,7 @@ Writes VLC's warnings and stats to vlc-ps5.log, for bug reports = Schrijft waars
|
||||
What VLC can read on this console = Wat VLC op deze console kan lezen
|
||||
Starts VLC again: it then sees drives plugged in since (when sandboxed) = Start VLC opnieuw: dan ziet het stations die sindsdien zijn aangesloten (in de sandbox)
|
||||
Version, credits and licences = Versie, makers en licenties
|
||||
The page that sends files and works as a remote, on your home network = De pagina die bestanden stuurt en als afstandsbediening werkt, in je thuisnetwerk
|
||||
The page that sends files and takes your OpenSubtitles key, on your home network = De pagina die bestanden verstuurt en je OpenSubtitles-sleutel ontvangt, op je thuisnetwerk
|
||||
Classic: panels and folders like a desktop player. Modern: black, VLC 4 style. VLC restarts = Klassiek: panelen en mappen zoals een desktopspeler. Modern: zwart, in VLC 4-stijl. VLC start opnieuw
|
||||
Your own free OpenSubtitles API key (and account), here or from the phone page = Je eigen gratis OpenSubtitles-API-sleutel (en account), hier of via de telefoonpagina
|
||||
The language of VLC's menus = De taal van de menu's van VLC
|
||||
@@ -228,16 +226,13 @@ Back right = Rechtsachter
|
||||
Each speaker plays a tone in turn. If one comes from the wrong place, tell us which. = Elke luidspreker speelt om de beurt een toon. Komt er een van de verkeerde plek, laat ons weten welke.
|
||||
Stop going round = Ronde stoppen
|
||||
Go round = Ronde starten
|
||||
Send files from your phone = Stuur bestanden van je telefoon
|
||||
Phone access is off. Turn it on in Settings > System. = Toegang via telefoon staat uit. Zet het aan in Instellingen > Systeem.
|
||||
The PS5 isn't connected to a network. Connect it, then come back. = De PS5 is niet met een netwerk verbonden. Verbind hem en kom dan terug.
|
||||
On a phone or computer on the same Wi-Fi, open this address in the browser: = Open op een telefoon of computer in hetzelfde wifi-netwerk dit adres in de browser:
|
||||
Send videos, music and subtitles to the PS5 = Stuur video's, muziek en ondertitels naar de PS5
|
||||
Use it as a remote while something plays = Gebruik het als afstandsbediening tijdens het afspelen
|
||||
Add your OpenSubtitles API key = Voeg je OpenSubtitles-API-sleutel toe
|
||||
Receiving %s = %s ontvangen
|
||||
Volume %d%% = Volume %d%%
|
||||
OpenSubtitles key received from your phone = OpenSubtitles-sleutel van je telefoon ontvangen
|
||||
A file arrived from your phone = Er is een bestand van je telefoon binnengekomen
|
||||
VLC media folder = Mediamap van VLC
|
||||
USB drive %d = USB-station %d
|
||||
USB drive (%s) = USB-station (%s)
|
||||
@@ -387,12 +382,10 @@ The API key works = De API-sleutel werkt
|
||||
Checking the key = Sleutel controleren
|
||||
The server didn't answer. = De server antwoordde niet.
|
||||
Couldn't open this place. = Kan deze locatie niet openen.
|
||||
File = Bestand
|
||||
Folder = Map
|
||||
Bitrate = Bitrate
|
||||
Container = Container
|
||||
Video = Video
|
||||
Track = Spoor
|
||||
White = Wit
|
||||
Yellow = Geel
|
||||
Cyan = Cyaan
|
||||
@@ -512,3 +505,49 @@ Skip = Overslaan
|
||||
Start = Starten
|
||||
Run setup again = Instellen opnieuw doen
|
||||
Language, look and playback, chosen again = Taal, uiterlijk en afspelen opnieuw kiezen
|
||||
Send files from a phone or PC = Bestanden van een telefoon of pc sturen
|
||||
Send files from your phone or PC = Stuur bestanden van je telefoon of pc
|
||||
OpenSubtitles key received from your phone or PC = OpenSubtitles-sleutel van je telefoon of pc ontvangen
|
||||
A file arrived from your phone or PC = Er is een bestand van je telefoon of pc binnengekomen
|
||||
File = Bestand
|
||||
%s file = %s-bestand
|
||||
OpenSubtitles refused this API key = OpenSubtitles heeft deze API-sleutel geweigerd
|
||||
New playlist = Nieuwe afspeellijst
|
||||
Made in VLC = Gemaakt in VLC
|
||||
Where = Locatie
|
||||
1 item = 1 item
|
||||
Removed from %s = Verwijderd uit %s
|
||||
This playlist is empty = Deze afspeellijst is leeg
|
||||
Create = Maken
|
||||
Play all = Alles afspelen
|
||||
Remove = Verwijderen
|
||||
Move = Verplaatsen
|
||||
Add to playlist = Toevoegen aan afspeellijst
|
||||
Rename playlist = Afspeellijst hernoemen
|
||||
Delete playlist = Afspeellijst verwijderen
|
||||
Name the playlist = Geef de afspeellijst een naam
|
||||
%s goes in first = %s gaat er als eerste in
|
||||
Type a name = Typ een naam
|
||||
Made %s = %s gemaakt
|
||||
Added to %s = Toegevoegd aan %s
|
||||
Already in %s = Staat al in %s
|
||||
Save = Opslaan
|
||||
%d playlists = %d afspeellijsten
|
||||
Saved in the media folder's Playlists, as a file VLC on a computer opens too = Opgeslagen in de map Playlists van de mediamap, als bestand dat VLC op een computer ook opent
|
||||
A playlist with this name is already there, or the disk can't be written = Er is al een afspeellijst met deze naam, of de schijf is niet beschrijfbaar
|
||||
Then add videos and songs from anywhere with the Options menu = Voeg daarna overal video's en nummers toe via het menu Opties
|
||||
Open the Options menu on a video or song and choose Add to playlist = Open het menu Opties bij een video of nummer en kies Toevoegen aan afspeellijst
|
||||
stereo = stereo
|
||||
Chapter %d = Hoofdstuk %d
|
||||
Audio %d = Audio %d
|
||||
Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. Subtitles: OpenSubtitles.com. = Lettertypen: Selawik, Inter, Noto, DejaVu (OFL). Zenderlijsten: iptv-org. Radio: radio-browser.info. Ondertitels: OpenSubtitles.com.
|
||||
15 min = 15 min
|
||||
30 min = 30 min
|
||||
45 min = 45 min
|
||||
60 min = 60 min
|
||||
90 min = 90 min
|
||||
Sign in to the share = Aanmelden bij de share
|
||||
%s needs a user name and password. = %s vraagt om een gebruikersnaam en wachtwoord.
|
||||
A Windows PC takes its own account; a NAS, the user you made on it. = Een Windows-pc gebruikt zijn eigen account; een NAS de gebruiker die je erop hebt gemaakt.
|
||||
Track %d = Track %d
|
||||
Track = Track
|
||||
+49
-10
@@ -146,13 +146,11 @@ About VLC = O VLC
|
||||
PS5 port = Port na PS5
|
||||
VideoLAN and the VLC contributors = VideoLAN i współtwórcy VLC
|
||||
Built with = Zbudowano z
|
||||
GPLv2+. An unofficial port, not affiliated with VideoLAN or Sony. = GPLv2+. Nieoficjalny port, niezwiązany z VideoLAN ani Sony.
|
||||
GPL-3.0. An unofficial port, not affiliated with VideoLAN or Sony. = GPL-3.0. Nieoficjalny port, niezwiązany z VideoLAN ani Sony.
|
||||
Resume = Wznów
|
||||
Audio, subtitles and more = Dźwięk, napisy i więcej
|
||||
Phone remote = Pilot w telefonie
|
||||
Settings = Ustawienia
|
||||
Stop playback = Zatrzymaj odtwarzanie
|
||||
Send files from a phone = Wyślij pliki z telefonu
|
||||
Rescan media = Przeskanuj multimedia
|
||||
Options = Opcje
|
||||
Found %zu files = Znalezione pliki: %zu
|
||||
@@ -188,7 +186,7 @@ Writes VLC's warnings and stats to vlc-ps5.log, for bug reports = Zapisuje ostrz
|
||||
What VLC can read on this console = Co VLC może czytać na tej konsoli
|
||||
Starts VLC again: it then sees drives plugged in since (when sandboxed) = Uruchamia VLC ponownie: zobaczy dyski podłączone w międzyczasie (w piaskownicy)
|
||||
Version, credits and licences = Wersja, twórcy i licencje
|
||||
The page that sends files and works as a remote, on your home network = Strona do wysyłania plików i sterowania, w Twojej sieci domowej
|
||||
The page that sends files and takes your OpenSubtitles key, on your home network = Strona do wysyłania plików i podania klucza OpenSubtitles, w Twojej sieci domowej
|
||||
Classic: panels and folders like a desktop player. Modern: black, VLC 4 style. VLC restarts = Klasyczny: panele i foldery jak w odtwarzaczu na komputer. Nowoczesny: czarny, w stylu VLC 4. VLC uruchomi się ponownie
|
||||
Your own free OpenSubtitles API key (and account), here or from the phone page = Twój darmowy klucz API OpenSubtitles (i konto), tutaj lub ze strony na telefonie
|
||||
The language of VLC's menus = Język menu VLC
|
||||
@@ -228,16 +226,13 @@ Back right = Tylny prawy
|
||||
Each speaker plays a tone in turn. If one comes from the wrong place, tell us which. = Każdy głośnik po kolei odtwarza dźwięk. Jeśli któryś dochodzi z niewłaściwego miejsca, daj nam znać który.
|
||||
Stop going round = Zatrzymaj obieg
|
||||
Go round = Rozpocznij obieg
|
||||
Send files from your phone = Wyślij pliki z telefonu
|
||||
Phone access is off. Turn it on in Settings > System. = Dostęp z telefonu jest wyłączony. Włącz go w Ustawienia > System.
|
||||
The PS5 isn't connected to a network. Connect it, then come back. = PS5 nie jest połączona z siecią. Połącz ją i wróć.
|
||||
On a phone or computer on the same Wi-Fi, open this address in the browser: = Na telefonie lub komputerze w tej samej sieci Wi-Fi otwórz ten adres w przeglądarce:
|
||||
Send videos, music and subtitles to the PS5 = Wysyłaj filmy, muzykę i napisy na PS5
|
||||
Use it as a remote while something plays = Używaj jako pilota podczas odtwarzania
|
||||
Add your OpenSubtitles API key = Dodaj swój klucz API OpenSubtitles
|
||||
Receiving %s = Odbieranie: %s
|
||||
Volume %d%% = Głośność %d%%
|
||||
OpenSubtitles key received from your phone = Odebrano klucz OpenSubtitles z telefonu
|
||||
A file arrived from your phone = Z telefonu dotarł plik
|
||||
VLC media folder = Folder multimediów VLC
|
||||
USB drive %d = Dysk USB %d
|
||||
USB drive (%s) = Dysk USB (%s)
|
||||
@@ -387,12 +382,10 @@ The API key works = Klucz API działa
|
||||
Checking the key = Sprawdzanie klucza
|
||||
The server didn't answer. = Serwer nie odpowiedział.
|
||||
Couldn't open this place. = Nie udało się otworzyć tego miejsca.
|
||||
File = Plik
|
||||
Folder = Folder
|
||||
Bitrate = Przepływność
|
||||
Container = Kontener
|
||||
Video = Wideo
|
||||
Track = Ścieżka
|
||||
White = Biały
|
||||
Yellow = Żółty
|
||||
Cyan = Cyjan
|
||||
@@ -512,3 +505,49 @@ Skip = Pomiń
|
||||
Start = Start
|
||||
Run setup again = Uruchom konfigurację ponownie
|
||||
Language, look and playback, chosen again = Język, wygląd i odtwarzanie od nowa
|
||||
Send files from a phone or PC = Wyślij pliki z telefonu lub komputera
|
||||
Send files from your phone or PC = Wyślij pliki z telefonu lub komputera
|
||||
OpenSubtitles key received from your phone or PC = Odebrano klucz OpenSubtitles z telefonu lub komputera
|
||||
A file arrived from your phone or PC = Z telefonu lub komputera dotarł plik
|
||||
File = Plik
|
||||
%s file = Plik %s
|
||||
OpenSubtitles refused this API key = OpenSubtitles odrzucił ten klucz API
|
||||
New playlist = Nowa playlista
|
||||
Made in VLC = Utworzona w VLC
|
||||
Where = Miejsce
|
||||
1 item = 1 element
|
||||
Removed from %s = Usunięto z %s
|
||||
This playlist is empty = Ta playlista jest pusta
|
||||
Create = Utwórz
|
||||
Play all = Odtwórz wszystko
|
||||
Remove = Usuń
|
||||
Move = Przenieś
|
||||
Add to playlist = Dodaj do playlisty
|
||||
Rename playlist = Zmień nazwę playlisty
|
||||
Delete playlist = Usuń playlistę
|
||||
Name the playlist = Nazwij playlistę
|
||||
%s goes in first = %s trafi tam pierwszy
|
||||
Type a name = Wpisz nazwę
|
||||
Made %s = Utworzono %s
|
||||
Added to %s = Dodano do %s
|
||||
Already in %s = Już jest w %s
|
||||
Save = Zapisz
|
||||
%d playlists = Playlisty: %d
|
||||
Saved in the media folder's Playlists, as a file VLC on a computer opens too = Zapisywana w folderze Playlists folderu multimediów, jako plik, który otworzy też VLC na komputerze
|
||||
A playlist with this name is already there, or the disk can't be written = Playlista o tej nazwie już istnieje albo nie można zapisać na dysku
|
||||
Then add videos and songs from anywhere with the Options menu = Potem dodawaj filmy i utwory z dowolnego miejsca przez menu Opcje
|
||||
Open the Options menu on a video or song and choose Add to playlist = Otwórz menu Opcje na filmie lub utworze i wybierz Dodaj do playlisty
|
||||
stereo = stereo
|
||||
Chapter %d = Rozdział %d
|
||||
Audio %d = Dźwięk %d
|
||||
Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. Subtitles: OpenSubtitles.com. = Czcionki: Selawik, Inter, Noto, DejaVu (OFL). Listy TV: iptv-org. Radio: radio-browser.info. Napisy: OpenSubtitles.com.
|
||||
15 min = 15 min
|
||||
30 min = 30 min
|
||||
45 min = 45 min
|
||||
60 min = 60 min
|
||||
90 min = 90 min
|
||||
Sign in to the share = Zaloguj się do udziału
|
||||
%s needs a user name and password. = %s wymaga nazwy użytkownika i hasła.
|
||||
A Windows PC takes its own account; a NAS, the user you made on it. = Komputer z Windows przyjmuje własne konto, a NAS użytkownika, którego na nim utworzono.
|
||||
Track %d = Ścieżka %d
|
||||
Track = Ścieżka
|
||||
+49
-10
@@ -146,13 +146,11 @@ About VLC = Sobre o VLC
|
||||
PS5 port = Versão para PS5
|
||||
VideoLAN and the VLC contributors = VideoLAN e os colaboradores do VLC
|
||||
Built with = Feito com
|
||||
GPLv2+. An unofficial port, not affiliated with VideoLAN or Sony. = GPLv2+. Uma versão não oficial, sem vínculo com a VideoLAN ou a Sony.
|
||||
GPL-3.0. An unofficial port, not affiliated with VideoLAN or Sony. = GPL-3.0. Uma versão não oficial, sem vínculo com a VideoLAN ou a Sony.
|
||||
Resume = Continuar
|
||||
Audio, subtitles and more = Áudio, legendas e mais
|
||||
Phone remote = Controle pelo celular
|
||||
Settings = Configurações
|
||||
Stop playback = Parar reprodução
|
||||
Send files from a phone = Enviar arquivos de um celular
|
||||
Rescan media = Procurar mídia de novo
|
||||
Options = Opções
|
||||
Found %zu files = %zu arquivos encontrados
|
||||
@@ -188,7 +186,7 @@ Writes VLC's warnings and stats to vlc-ps5.log, for bug reports = Grava avisos e
|
||||
What VLC can read on this console = O que o VLC pode ler neste console
|
||||
Starts VLC again: it then sees drives plugged in since (when sandboxed) = Reinicia o VLC: ele passa a ver unidades conectadas depois (no modo isolado)
|
||||
Version, credits and licences = Versão, créditos e licenças
|
||||
The page that sends files and works as a remote, on your home network = A página que envia arquivos e funciona como controle, na sua rede de casa
|
||||
The page that sends files and takes your OpenSubtitles key, on your home network = A página que envia arquivos e recebe sua chave do OpenSubtitles, na sua rede doméstica
|
||||
Classic: panels and folders like a desktop player. Modern: black, VLC 4 style. VLC restarts = Clássico: painéis e pastas como um player de computador. Moderno: preto, estilo VLC 4. O VLC reinicia
|
||||
Your own free OpenSubtitles API key (and account), here or from the phone page = Sua própria chave de API gratuita do OpenSubtitles (e conta), aqui ou pela página do celular
|
||||
The language of VLC's menus = O idioma dos menus do VLC
|
||||
@@ -228,16 +226,13 @@ Back right = Traseiro direito
|
||||
Each speaker plays a tone in turn. If one comes from the wrong place, tell us which. = Cada alto-falante toca um tom por vez. Se algum vier do lugar errado, conte-nos qual.
|
||||
Stop going round = Parar a volta
|
||||
Go round = Dar a volta
|
||||
Send files from your phone = Envie arquivos do seu celular
|
||||
Phone access is off. Turn it on in Settings > System. = O acesso pelo celular está desligado. Ligue em Configurações > Sistema.
|
||||
The PS5 isn't connected to a network. Connect it, then come back. = O PS5 não está conectado a uma rede. Conecte e volte aqui.
|
||||
On a phone or computer on the same Wi-Fi, open this address in the browser: = Em um celular ou computador no mesmo Wi-Fi, abra este endereço no navegador:
|
||||
Send videos, music and subtitles to the PS5 = Envie vídeos, músicas e legendas para o PS5
|
||||
Use it as a remote while something plays = Use como controle enquanto algo toca
|
||||
Add your OpenSubtitles API key = Adicione sua chave de API do OpenSubtitles
|
||||
Receiving %s = Recebendo %s
|
||||
Volume %d%% = Volume %d%%
|
||||
OpenSubtitles key received from your phone = Chave do OpenSubtitles recebida do seu celular
|
||||
A file arrived from your phone = Chegou um arquivo do seu celular
|
||||
VLC media folder = Pasta de mídia do VLC
|
||||
USB drive %d = Pendrive %d
|
||||
USB drive (%s) = Pendrive (%s)
|
||||
@@ -387,12 +382,10 @@ The API key works = A chave de API funciona
|
||||
Checking the key = A verificação da chave
|
||||
The server didn't answer. = O servidor não respondeu.
|
||||
Couldn't open this place. = Não foi possível abrir este local.
|
||||
File = Arquivo
|
||||
Folder = Pasta
|
||||
Bitrate = Taxa de bits
|
||||
Container = Contêiner
|
||||
Video = Vídeo
|
||||
Track = Faixa
|
||||
White = Branco
|
||||
Yellow = Amarelo
|
||||
Cyan = Ciano
|
||||
@@ -512,3 +505,49 @@ Skip = Pular
|
||||
Start = Começar
|
||||
Run setup again = Refazer a configuração
|
||||
Language, look and playback, chosen again = Idioma, aparência e reprodução, de novo
|
||||
Send files from a phone or PC = Enviar arquivos de um celular ou PC
|
||||
Send files from your phone or PC = Envie arquivos do seu celular ou PC
|
||||
OpenSubtitles key received from your phone or PC = Chave do OpenSubtitles recebida do seu celular ou PC
|
||||
A file arrived from your phone or PC = Chegou um arquivo do seu celular ou PC
|
||||
File = Arquivo
|
||||
%s file = Arquivo %s
|
||||
OpenSubtitles refused this API key = O OpenSubtitles recusou esta chave de API
|
||||
New playlist = Nova playlist
|
||||
Made in VLC = Criada no VLC
|
||||
Where = Local
|
||||
1 item = 1 item
|
||||
Removed from %s = Removido de %s
|
||||
This playlist is empty = Esta playlist está vazia
|
||||
Create = Criar
|
||||
Play all = Tocar tudo
|
||||
Remove = Remover
|
||||
Move = Mover
|
||||
Add to playlist = Adicionar à playlist
|
||||
Rename playlist = Renomear playlist
|
||||
Delete playlist = Excluir playlist
|
||||
Name the playlist = Dê um nome à playlist
|
||||
%s goes in first = %s entra primeiro
|
||||
Type a name = Digite um nome
|
||||
Made %s = %s criada
|
||||
Added to %s = Adicionado a %s
|
||||
Already in %s = Já está em %s
|
||||
Save = Salvar
|
||||
%d playlists = %d playlists
|
||||
Saved in the media folder's Playlists, as a file VLC on a computer opens too = Salva na pasta Playlists da pasta de mídia, como um arquivo que o VLC no computador também abre
|
||||
A playlist with this name is already there, or the disk can't be written = Já existe uma playlist com este nome, ou não dá para gravar no disco
|
||||
Then add videos and songs from anywhere with the Options menu = Depois adicione vídeos e músicas de qualquer lugar pelo menu Opções
|
||||
Open the Options menu on a video or song and choose Add to playlist = Abra o menu Opções em um vídeo ou música e escolha Adicionar à playlist
|
||||
stereo = estéreo
|
||||
Chapter %d = Capítulo %d
|
||||
Audio %d = Áudio %d
|
||||
Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. Subtitles: OpenSubtitles.com. = Fontes: Selawik, Inter, Noto, DejaVu (OFL). Listas de TV: iptv-org. Rádio: radio-browser.info. Legendas: OpenSubtitles.com.
|
||||
15 min = 15 min
|
||||
30 min = 30 min
|
||||
45 min = 45 min
|
||||
60 min = 60 min
|
||||
90 min = 90 min
|
||||
Sign in to the share = Entrar no compartilhamento
|
||||
%s needs a user name and password. = %s pede um usuário e uma senha.
|
||||
A Windows PC takes its own account; a NAS, the user you made on it. = Um PC com Windows usa a própria conta; um NAS, o usuário que você criou nele.
|
||||
Track %d = Faixa %d
|
||||
Track = Faixa
|
||||
+49
-10
@@ -146,13 +146,11 @@ About VLC = О VLC
|
||||
PS5 port = Порт для PS5
|
||||
VideoLAN and the VLC contributors = VideoLAN и участники проекта VLC
|
||||
Built with = Создано с помощью
|
||||
GPLv2+. An unofficial port, not affiliated with VideoLAN or Sony. = GPLv2+. Неофициальный порт, не связанный с VideoLAN или Sony.
|
||||
GPL-3.0. An unofficial port, not affiliated with VideoLAN or Sony. = GPL-3.0. Неофициальный порт, не связанный с VideoLAN или Sony.
|
||||
Resume = Продолжить
|
||||
Audio, subtitles and more = Звук, субтитры и другое
|
||||
Phone remote = Пульт на телефоне
|
||||
Settings = Настройки
|
||||
Stop playback = Остановить воспроизведение
|
||||
Send files from a phone = Отправить файлы с телефона
|
||||
Rescan media = Пересканировать медиа
|
||||
Options = Параметры
|
||||
Found %zu files = Найдено файлов: %zu
|
||||
@@ -188,7 +186,7 @@ Writes VLC's warnings and stats to vlc-ps5.log, for bug reports = Записыв
|
||||
What VLC can read on this console = Что VLC может читать на этой консоли
|
||||
Starts VLC again: it then sees drives plugged in since (when sandboxed) = Перезапускает VLC: так он увидит подключённые позже диски (в песочнице)
|
||||
Version, credits and licences = Версия, авторы и лицензии
|
||||
The page that sends files and works as a remote, on your home network = Страница для отправки файлов и управления, в вашей домашней сети
|
||||
The page that sends files and takes your OpenSubtitles key, on your home network = Страница для отправки файлов и ввода ключа OpenSubtitles в вашей домашней сети
|
||||
Classic: panels and folders like a desktop player. Modern: black, VLC 4 style. VLC restarts = Классический: панели и папки как в настольном плеере. Современный: чёрный, в стиле VLC 4. VLC перезапустится
|
||||
Your own free OpenSubtitles API key (and account), here or from the phone page = Ваш бесплатный ключ API OpenSubtitles (и аккаунт), здесь или со страницы для телефона
|
||||
The language of VLC's menus = Язык меню VLC
|
||||
@@ -228,16 +226,13 @@ Back right = Задний правый
|
||||
Each speaker plays a tone in turn. If one comes from the wrong place, tell us which. = Каждый динамик по очереди издаёт сигнал. Если звук идёт не оттуда, сообщите нам какой.
|
||||
Stop going round = Остановить обход
|
||||
Go round = Начать обход
|
||||
Send files from your phone = Отправьте файлы с телефона
|
||||
Phone access is off. Turn it on in Settings > System. = Доступ с телефона выключен. Включите его в Настройки > Система.
|
||||
The PS5 isn't connected to a network. Connect it, then come back. = PS5 не подключена к сети. Подключите её и вернитесь.
|
||||
On a phone or computer on the same Wi-Fi, open this address in the browser: = На телефоне или компьютере в той же сети Wi-Fi откройте этот адрес в браузере:
|
||||
Send videos, music and subtitles to the PS5 = Отправляйте на PS5 видео, музыку и субтитры
|
||||
Use it as a remote while something plays = Используйте как пульт во время воспроизведения
|
||||
Add your OpenSubtitles API key = Добавьте свой API-ключ OpenSubtitles
|
||||
Receiving %s = Получение: %s
|
||||
Volume %d%% = Громкость %d%%
|
||||
OpenSubtitles key received from your phone = Ключ OpenSubtitles получен с телефона
|
||||
A file arrived from your phone = С телефона пришёл файл
|
||||
VLC media folder = Медиапапка VLC
|
||||
USB drive %d = USB-накопитель %d
|
||||
USB drive (%s) = USB-накопитель (%s)
|
||||
@@ -387,12 +382,10 @@ The API key works = Ключ API работает
|
||||
Checking the key = Проверка ключа
|
||||
The server didn't answer. = Сервер не ответил.
|
||||
Couldn't open this place. = Не удалось открыть это место.
|
||||
File = Файл
|
||||
Folder = Папка
|
||||
Bitrate = Битрейт
|
||||
Container = Контейнер
|
||||
Video = Видео
|
||||
Track = Дорожка
|
||||
White = Белый
|
||||
Yellow = Жёлтый
|
||||
Cyan = Голубой
|
||||
@@ -512,3 +505,49 @@ Skip = Пропустить
|
||||
Start = Начать
|
||||
Run setup again = Пройти настройку снова
|
||||
Language, look and playback, chosen again = Язык, оформление и воспроизведение заново
|
||||
Send files from a phone or PC = Отправить файлы с телефона или ПК
|
||||
Send files from your phone or PC = Отправьте файлы с телефона или ПК
|
||||
OpenSubtitles key received from your phone or PC = Ключ OpenSubtitles получен с телефона или ПК
|
||||
A file arrived from your phone or PC = С телефона или ПК пришёл файл
|
||||
File = Файл
|
||||
%s file = Файл %s
|
||||
OpenSubtitles refused this API key = OpenSubtitles отклонил этот API-ключ
|
||||
New playlist = Новый плейлист
|
||||
Made in VLC = Создан в VLC
|
||||
Where = Где
|
||||
1 item = 1 элемент
|
||||
Removed from %s = Удалено из %s
|
||||
This playlist is empty = Этот плейлист пуст
|
||||
Create = Создать
|
||||
Play all = Воспроизвести все
|
||||
Remove = Убрать
|
||||
Move = Переместить
|
||||
Add to playlist = Добавить в плейлист
|
||||
Rename playlist = Переименовать плейлист
|
||||
Delete playlist = Удалить плейлист
|
||||
Name the playlist = Назовите плейлист
|
||||
%s goes in first = Первым в него попадёт %s
|
||||
Type a name = Введите название
|
||||
Made %s = Создан %s
|
||||
Added to %s = Добавлено в %s
|
||||
Already in %s = Уже есть в %s
|
||||
Save = Сохранить
|
||||
%d playlists = Плейлистов: %d
|
||||
Saved in the media folder's Playlists, as a file VLC on a computer opens too = Сохраняется в папке Playlists папки медиа, файлом, который открывает и VLC на компьютере
|
||||
A playlist with this name is already there, or the disk can't be written = Плейлист с таким названием уже есть, или на диск нельзя записать
|
||||
Then add videos and songs from anywhere with the Options menu = Потом добавляйте видео и песни откуда угодно через меню «Параметры»
|
||||
Open the Options menu on a video or song and choose Add to playlist = Откройте меню «Параметры» на видео или песне и выберите Добавить в плейлист
|
||||
stereo = стерео
|
||||
Chapter %d = Глава %d
|
||||
Audio %d = Аудио %d
|
||||
Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. Subtitles: OpenSubtitles.com. = Шрифты: Selawik, Inter, Noto, DejaVu (OFL). Списки каналов: iptv-org. Радио: radio-browser.info. Субтитры: OpenSubtitles.com.
|
||||
15 min = 15 мин
|
||||
30 min = 30 мин
|
||||
45 min = 45 мин
|
||||
60 min = 60 мин
|
||||
90 min = 90 мин
|
||||
Sign in to the share = Вход в общую папку
|
||||
%s needs a user name and password. = %s требует имя пользователя и пароль.
|
||||
A Windows PC takes its own account; a NAS, the user you made on it. = Компьютер с Windows принимает свою учётную запись, NAS — пользователя, которого вы на нём создали.
|
||||
Track %d = Дорожка %d
|
||||
Track = Дорожка
|
||||
+49
-10
@@ -146,13 +146,11 @@ About VLC = VLC hakkında
|
||||
PS5 port = PS5 uyarlaması
|
||||
VideoLAN and the VLC contributors = VideoLAN ve VLC katkıcıları
|
||||
Built with = Kullanılanlar
|
||||
GPLv2+. An unofficial port, not affiliated with VideoLAN or Sony. = GPLv2+. Resmî olmayan bir uyarlama; VideoLAN ya da Sony ile ilgisi yoktur.
|
||||
GPL-3.0. An unofficial port, not affiliated with VideoLAN or Sony. = GPL-3.0. Resmî olmayan bir uyarlama; VideoLAN ya da Sony ile ilgisi yoktur.
|
||||
Resume = Devam et
|
||||
Audio, subtitles and more = Ses, altyazı ve daha fazlası
|
||||
Phone remote = Telefon kumandası
|
||||
Settings = Ayarlar
|
||||
Stop playback = Oynatmayı durdur
|
||||
Send files from a phone = Telefondan dosya gönder
|
||||
Rescan media = Medyayı yeniden tara
|
||||
Options = Seçenekler
|
||||
Found %zu files = %zu dosya bulundu
|
||||
@@ -188,7 +186,7 @@ Writes VLC's warnings and stats to vlc-ps5.log, for bug reports = Hata bildiriml
|
||||
What VLC can read on this console = VLC'nin bu konsolda okuyabildikleri
|
||||
Starts VLC again: it then sees drives plugged in since (when sandboxed) = VLC'yi yeniden başlatır: böylece sonradan takılan sürücüleri görür (korumalı alanda)
|
||||
Version, credits and licences = Sürüm, emeği geçenler ve lisanslar
|
||||
The page that sends files and works as a remote, on your home network = Ev ağınızda dosya gönderen ve kumanda işlevi gören sayfa
|
||||
The page that sends files and takes your OpenSubtitles key, on your home network = Ev ağınızda dosya gönderen ve OpenSubtitles anahtarınızı alan sayfa
|
||||
Classic: panels and folders like a desktop player. Modern: black, VLC 4 style. VLC restarts = Klasik: masaüstü oynatıcı gibi paneller ve klasörler. Modern: siyah, VLC 4 tarzı. VLC yeniden başlar
|
||||
Your own free OpenSubtitles API key (and account), here or from the phone page = Kendi ücretsiz OpenSubtitles API anahtarınız (ve hesabınız), burada ya da telefon sayfasından
|
||||
The language of VLC's menus = VLC menülerinin dili
|
||||
@@ -228,16 +226,13 @@ Back right = Arka sağ
|
||||
Each speaker plays a tone in turn. If one comes from the wrong place, tell us which. = Her hoparlör sırayla bir ses çalar. Biri yanlış yerden geliyorsa hangisi olduğunu bize bildirin.
|
||||
Stop going round = Turu durdur
|
||||
Go round = Tur at
|
||||
Send files from your phone = Telefonunuzdan dosya gönderin
|
||||
Phone access is off. Turn it on in Settings > System. = Telefon erişimi kapalı. Ayarlar > Sistem bölümünden açın.
|
||||
The PS5 isn't connected to a network. Connect it, then come back. = PS5 bir ağa bağlı değil. Bağlayıp geri gelin.
|
||||
On a phone or computer on the same Wi-Fi, open this address in the browser: = Aynı Wi-Fi'deki bir telefon ya da bilgisayarda bu adresi tarayıcıda açın:
|
||||
Send videos, music and subtitles to the PS5 = PS5'e video, müzik ve altyazı gönderin
|
||||
Use it as a remote while something plays = Bir şey oynarken kumanda olarak kullanın
|
||||
Add your OpenSubtitles API key = OpenSubtitles API anahtarınızı ekleyin
|
||||
Receiving %s = %s alınıyor
|
||||
Volume %d%% = Ses %%%d
|
||||
OpenSubtitles key received from your phone = OpenSubtitles anahtarı telefonunuzdan alındı
|
||||
A file arrived from your phone = Telefonunuzdan bir dosya geldi
|
||||
VLC media folder = VLC medya klasörü
|
||||
USB drive %d = USB sürücü %d
|
||||
USB drive (%s) = USB sürücü (%s)
|
||||
@@ -387,12 +382,10 @@ The API key works = API anahtarı çalışıyor
|
||||
Checking the key = Anahtar denetimi
|
||||
The server didn't answer. = Sunucu yanıt vermedi.
|
||||
Couldn't open this place. = Bu konum açılamadı.
|
||||
File = Dosya
|
||||
Folder = Klasör
|
||||
Bitrate = Bit hızı
|
||||
Container = Kapsayıcı
|
||||
Video = Video
|
||||
Track = Parça
|
||||
White = Beyaz
|
||||
Yellow = Sarı
|
||||
Cyan = Camgöbeği
|
||||
@@ -512,3 +505,49 @@ Skip = Atla
|
||||
Start = Başla
|
||||
Run setup again = Kurulumu yeniden başlat
|
||||
Language, look and playback, chosen again = Dil, görünüm ve oynatma yeniden seçilir
|
||||
Send files from a phone or PC = Telefondan veya bilgisayardan dosya gönder
|
||||
Send files from your phone or PC = Telefonunuzdan veya bilgisayarınızdan dosya gönderin
|
||||
OpenSubtitles key received from your phone or PC = OpenSubtitles anahtarı telefonunuzdan veya bilgisayarınızdan alındı
|
||||
A file arrived from your phone or PC = Telefonunuzdan veya bilgisayarınızdan bir dosya geldi
|
||||
File = Dosya
|
||||
%s file = %s dosyası
|
||||
OpenSubtitles refused this API key = OpenSubtitles bu API anahtarını reddetti
|
||||
New playlist = Yeni çalma listesi
|
||||
Made in VLC = VLC'de oluşturuldu
|
||||
Where = Konum
|
||||
1 item = 1 öge
|
||||
Removed from %s = %s listesinden çıkarıldı
|
||||
This playlist is empty = Bu çalma listesi boş
|
||||
Create = Oluştur
|
||||
Play all = Tümünü oynat
|
||||
Remove = Çıkar
|
||||
Move = Taşı
|
||||
Add to playlist = Çalma listesine ekle
|
||||
Rename playlist = Çalma listesini yeniden adlandır
|
||||
Delete playlist = Çalma listesini sil
|
||||
Name the playlist = Çalma listesine ad verin
|
||||
%s goes in first = İlk olarak %s eklenir
|
||||
Type a name = Bir ad yazın
|
||||
Made %s = %s oluşturuldu
|
||||
Added to %s = %s listesine eklendi
|
||||
Already in %s = Zaten %s içinde
|
||||
Save = Kaydet
|
||||
%d playlists = %d çalma listesi
|
||||
Saved in the media folder's Playlists, as a file VLC on a computer opens too = Medya klasörünün Playlists klasörüne, bilgisayardaki VLC'nin de açtığı bir dosya olarak kaydedilir
|
||||
A playlist with this name is already there, or the disk can't be written = Bu adda bir çalma listesi zaten var ya da diske yazılamıyor
|
||||
Then add videos and songs from anywhere with the Options menu = Sonra her yerden Seçenekler menüsüyle video ve şarkı ekleyin
|
||||
Open the Options menu on a video or song and choose Add to playlist = Bir video veya şarkıda Seçenekler menüsünü açıp Çalma listesine ekle'yi seçin
|
||||
stereo = stereo
|
||||
Chapter %d = Bölüm %d
|
||||
Audio %d = Ses %d
|
||||
Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. Subtitles: OpenSubtitles.com. = Yazı tipleri: Selawik, Inter, Noto, DejaVu (OFL). TV listeleri: iptv-org. Radyo: radio-browser.info. Altyazılar: OpenSubtitles.com.
|
||||
15 min = 15 dk
|
||||
30 min = 30 dk
|
||||
45 min = 45 dk
|
||||
60 min = 60 dk
|
||||
90 min = 90 dk
|
||||
Sign in to the share = Paylaşıma giriş yap
|
||||
%s needs a user name and password. = %s bir kullanıcı adı ve parola istiyor.
|
||||
A Windows PC takes its own account; a NAS, the user you made on it. = Windows PC kendi hesabını, NAS ise üzerinde oluşturduğunuz kullanıcıyı kabul eder.
|
||||
Track %d = Parça %d
|
||||
Track = Parça
|
||||
+49
-10
@@ -146,13 +146,11 @@ About VLC = 关于 VLC
|
||||
PS5 port = PS5 移植
|
||||
VideoLAN and the VLC contributors = VideoLAN 和 VLC 贡献者
|
||||
Built with = 使用的组件
|
||||
GPLv2+. An unofficial port, not affiliated with VideoLAN or Sony. = GPLv2+。非官方移植版,与 VideoLAN 和 Sony 无关。
|
||||
GPL-3.0. An unofficial port, not affiliated with VideoLAN or Sony. = GPL-3.0。非官方移植版,与 VideoLAN 和 Sony 无关。
|
||||
Resume = 继续
|
||||
Audio, subtitles and more = 音频、字幕等
|
||||
Phone remote = 手机遥控
|
||||
Settings = 设置
|
||||
Stop playback = 停止播放
|
||||
Send files from a phone = 从手机发送文件
|
||||
Rescan media = 重新扫描媒体
|
||||
Options = 选项
|
||||
Found %zu files = 找到 %zu 个文件
|
||||
@@ -188,7 +186,7 @@ Writes VLC's warnings and stats to vlc-ps5.log, for bug reports = 将 VLC 的警
|
||||
What VLC can read on this console = VLC 在本主机上可读取的内容
|
||||
Starts VLC again: it then sees drives plugged in since (when sandboxed) = 重新启动 VLC:之后能看到期间插入的驱动器(沙盒模式下)
|
||||
Version, credits and licences = 版本、致谢和许可证
|
||||
The page that sends files and works as a remote, on your home network = 在家庭网络中发送文件并充当遥控器的页面
|
||||
The page that sends files and takes your OpenSubtitles key, on your home network = 在家庭网络中发送文件并接收 OpenSubtitles 密钥的页面
|
||||
Classic: panels and folders like a desktop player. Modern: black, VLC 4 style. VLC restarts = 经典:像桌面播放器一样的面板和文件夹。现代:黑色,VLC 4 风格。VLC 将重新启动
|
||||
Your own free OpenSubtitles API key (and account), here or from the phone page = 你自己的免费 OpenSubtitles API 密钥(及账号),在此处或手机页面填写
|
||||
The language of VLC's menus = VLC 菜单的语言
|
||||
@@ -228,16 +226,13 @@ Back right = 后右
|
||||
Each speaker plays a tone in turn. If one comes from the wrong place, tell us which. = 每个扬声器依次发出提示音。如果有声音来自错误的位置,请告诉我们是哪一个。
|
||||
Stop going round = 停止轮询
|
||||
Go round = 开始轮询
|
||||
Send files from your phone = 从你的手机发送文件
|
||||
Phone access is off. Turn it on in Settings > System. = 手机访问已关闭。请在 设置 > 系统 中开启。
|
||||
The PS5 isn't connected to a network. Connect it, then come back. = PS5 未连接网络。连接后再回来。
|
||||
On a phone or computer on the same Wi-Fi, open this address in the browser: = 在同一 Wi-Fi 下的手机或电脑浏览器中打开此地址:
|
||||
Send videos, music and subtitles to the PS5 = 将视频、音乐和字幕发送到 PS5
|
||||
Use it as a remote while something plays = 播放时可当作遥控器使用
|
||||
Add your OpenSubtitles API key = 添加你的 OpenSubtitles API 密钥
|
||||
Receiving %s = 正在接收 %s
|
||||
Volume %d%% = 音量 %d%%
|
||||
OpenSubtitles key received from your phone = 已从手机收到 OpenSubtitles 密钥
|
||||
A file arrived from your phone = 收到来自手机的文件
|
||||
VLC media folder = VLC 媒体文件夹
|
||||
USB drive %d = USB 驱动器 %d
|
||||
USB drive (%s) = USB 驱动器(%s)
|
||||
@@ -387,12 +382,10 @@ The API key works = API 密钥有效
|
||||
Checking the key = 验证密钥
|
||||
The server didn't answer. = 服务器没有响应。
|
||||
Couldn't open this place. = 无法打开此位置。
|
||||
File = 文件
|
||||
Folder = 文件夹
|
||||
Bitrate = 码率
|
||||
Container = 封装格式
|
||||
Video = 视频
|
||||
Track = 轨道
|
||||
White = 白色
|
||||
Yellow = 黄色
|
||||
Cyan = 青色
|
||||
@@ -512,3 +505,49 @@ Skip = 跳过
|
||||
Start = 开始
|
||||
Run setup again = 重新进行设置
|
||||
Language, look and playback, chosen again = 重新选择语言、外观和播放
|
||||
Send files from a phone or PC = 从手机或电脑发送文件
|
||||
Send files from your phone or PC = 从你的手机或电脑发送文件
|
||||
OpenSubtitles key received from your phone or PC = 已从手机或电脑收到 OpenSubtitles 密钥
|
||||
A file arrived from your phone or PC = 收到来自手机或电脑的文件
|
||||
File = 文件
|
||||
%s file = %s 文件
|
||||
OpenSubtitles refused this API key = OpenSubtitles 拒绝了此 API 密钥
|
||||
New playlist = 新建播放列表
|
||||
Made in VLC = 在 VLC 中创建
|
||||
Where = 位置
|
||||
1 item = 1 项
|
||||
Removed from %s = 已从 %s 移除
|
||||
This playlist is empty = 这个播放列表是空的
|
||||
Create = 创建
|
||||
Play all = 全部播放
|
||||
Remove = 移除
|
||||
Move = 移动
|
||||
Add to playlist = 添加到播放列表
|
||||
Rename playlist = 重命名播放列表
|
||||
Delete playlist = 删除播放列表
|
||||
Name the playlist = 给播放列表命名
|
||||
%s goes in first = %s 会最先加入
|
||||
Type a name = 请输入名称
|
||||
Made %s = 已创建 %s
|
||||
Added to %s = 已添加到 %s
|
||||
Already in %s = 已在 %s 中
|
||||
Save = 保存
|
||||
%d playlists = %d 个播放列表
|
||||
Saved in the media folder's Playlists, as a file VLC on a computer opens too = 保存在媒体文件夹的 Playlists 中,电脑上的 VLC 也能打开
|
||||
A playlist with this name is already there, or the disk can't be written = 已有同名播放列表,或磁盘无法写入
|
||||
Then add videos and songs from anywhere with the Options menu = 之后在任何地方用选项菜单添加视频和歌曲
|
||||
Open the Options menu on a video or song and choose Add to playlist = 在视频或歌曲上打开选项菜单,然后选择添加到播放列表
|
||||
stereo = 立体声
|
||||
Chapter %d = 第 %d 章
|
||||
Audio %d = 音轨 %d
|
||||
Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. Subtitles: OpenSubtitles.com. = 字体:Selawik、Inter、Noto、DejaVu (OFL)。电视列表:iptv-org。电台:radio-browser.info。字幕:OpenSubtitles.com。
|
||||
15 min = 15 分钟
|
||||
30 min = 30 分钟
|
||||
45 min = 45 分钟
|
||||
60 min = 60 分钟
|
||||
90 min = 90 分钟
|
||||
Sign in to the share = 登录共享文件夹
|
||||
%s needs a user name and password. = %s 需要用户名和密码。
|
||||
A Windows PC takes its own account; a NAS, the user you made on it. = Windows 电脑用它自己的账户;NAS 用你在上面创建的用户。
|
||||
Track %d = 音轨 %d
|
||||
Track = 音轨
|
||||
+49
-10
@@ -146,13 +146,11 @@ About VLC = 關於 VLC
|
||||
PS5 port = PS5 移植
|
||||
VideoLAN and the VLC contributors = VideoLAN 與 VLC 貢獻者
|
||||
Built with = 使用的元件
|
||||
GPLv2+. An unofficial port, not affiliated with VideoLAN or Sony. = GPLv2+。非官方移植版,與 VideoLAN 及 Sony 無關。
|
||||
GPL-3.0. An unofficial port, not affiliated with VideoLAN or Sony. = GPL-3.0。非官方移植版,與 VideoLAN 及 Sony 無關。
|
||||
Resume = 繼續
|
||||
Audio, subtitles and more = 音訊、字幕等
|
||||
Phone remote = 手機遙控
|
||||
Settings = 設定
|
||||
Stop playback = 停止播放
|
||||
Send files from a phone = 從手機傳送檔案
|
||||
Rescan media = 重新掃描媒體
|
||||
Options = 選項
|
||||
Found %zu files = 找到 %zu 個檔案
|
||||
@@ -188,7 +186,7 @@ Writes VLC's warnings and stats to vlc-ps5.log, for bug reports = 將 VLC 的警
|
||||
What VLC can read on this console = VLC 在本主機上可讀取的內容
|
||||
Starts VLC again: it then sees drives plugged in since (when sandboxed) = 重新啟動 VLC:之後能看到期間插入的磁碟(沙箱模式下)
|
||||
Version, credits and licences = 版本、致謝與授權
|
||||
The page that sends files and works as a remote, on your home network = 在家用網路中傳送檔案並當作遙控器的頁面
|
||||
The page that sends files and takes your OpenSubtitles key, on your home network = 在家用網路中傳送檔案並接收 OpenSubtitles 金鑰的頁面
|
||||
Classic: panels and folders like a desktop player. Modern: black, VLC 4 style. VLC restarts = 經典:像桌面播放器的面板與資料夾。現代:黑色,VLC 4 風格。VLC 將重新啟動
|
||||
Your own free OpenSubtitles API key (and account), here or from the phone page = 你自己的免費 OpenSubtitles API 金鑰(及帳號),可在此或手機頁面填寫
|
||||
The language of VLC's menus = VLC 選單的語言
|
||||
@@ -228,16 +226,13 @@ Back right = 後右
|
||||
Each speaker plays a tone in turn. If one comes from the wrong place, tell us which. = 每個喇叭依序發出提示音。如果有聲音來自錯誤的位置,請告訴我們是哪一個。
|
||||
Stop going round = 停止輪播
|
||||
Go round = 開始輪播
|
||||
Send files from your phone = 從你的手機傳送檔案
|
||||
Phone access is off. Turn it on in Settings > System. = 手機存取已關閉。請在 設定 > 系統 中開啟。
|
||||
The PS5 isn't connected to a network. Connect it, then come back. = PS5 未連上網路。連線後再回來。
|
||||
On a phone or computer on the same Wi-Fi, open this address in the browser: = 在同一 Wi-Fi 的手機或電腦瀏覽器中開啟此位址:
|
||||
Send videos, music and subtitles to the PS5 = 將影片、音樂與字幕傳送到 PS5
|
||||
Use it as a remote while something plays = 播放時可當作遙控器
|
||||
Add your OpenSubtitles API key = 新增你的 OpenSubtitles API 金鑰
|
||||
Receiving %s = 正在接收 %s
|
||||
Volume %d%% = 音量 %d%%
|
||||
OpenSubtitles key received from your phone = 已從手機收到 OpenSubtitles 金鑰
|
||||
A file arrived from your phone = 收到來自手機的檔案
|
||||
VLC media folder = VLC 媒體資料夾
|
||||
USB drive %d = USB 隨身碟 %d
|
||||
USB drive (%s) = USB 隨身碟(%s)
|
||||
@@ -387,12 +382,10 @@ The API key works = API 金鑰有效
|
||||
Checking the key = 驗證金鑰
|
||||
The server didn't answer. = 伺服器沒有回應。
|
||||
Couldn't open this place. = 無法開啟此位置。
|
||||
File = 檔案
|
||||
Folder = 資料夾
|
||||
Bitrate = 位元率
|
||||
Container = 封裝格式
|
||||
Video = 影片
|
||||
Track = 軌道
|
||||
White = 白色
|
||||
Yellow = 黃色
|
||||
Cyan = 青色
|
||||
@@ -512,3 +505,49 @@ Skip = 略過
|
||||
Start = 開始
|
||||
Run setup again = 重新進行設定
|
||||
Language, look and playback, chosen again = 重新選擇語言、外觀與播放
|
||||
Send files from a phone or PC = 從手機或電腦傳送檔案
|
||||
Send files from your phone or PC = 從你的手機或電腦傳送檔案
|
||||
OpenSubtitles key received from your phone or PC = 已從手機或電腦收到 OpenSubtitles 金鑰
|
||||
A file arrived from your phone or PC = 收到來自手機或電腦的檔案
|
||||
File = 檔案
|
||||
%s file = %s 檔案
|
||||
OpenSubtitles refused this API key = OpenSubtitles 拒絕了此 API 金鑰
|
||||
New playlist = 新增播放清單
|
||||
Made in VLC = 在 VLC 中建立
|
||||
Where = 位置
|
||||
1 item = 1 項
|
||||
Removed from %s = 已從 %s 移除
|
||||
This playlist is empty = 這個播放清單是空的
|
||||
Create = 建立
|
||||
Play all = 全部播放
|
||||
Remove = 移除
|
||||
Move = 移動
|
||||
Add to playlist = 加入播放清單
|
||||
Rename playlist = 重新命名播放清單
|
||||
Delete playlist = 刪除播放清單
|
||||
Name the playlist = 為播放清單命名
|
||||
%s goes in first = %s 會最先加入
|
||||
Type a name = 請輸入名稱
|
||||
Made %s = 已建立 %s
|
||||
Added to %s = 已加入 %s
|
||||
Already in %s = 已在 %s 中
|
||||
Save = 儲存
|
||||
%d playlists = %d 個播放清單
|
||||
Saved in the media folder's Playlists, as a file VLC on a computer opens too = 儲存在媒體資料夾的 Playlists 中,電腦上的 VLC 也能開啟
|
||||
A playlist with this name is already there, or the disk can't be written = 已有同名的播放清單,或磁碟無法寫入
|
||||
Then add videos and songs from anywhere with the Options menu = 之後在任何地方用選項選單加入影片和歌曲
|
||||
Open the Options menu on a video or song and choose Add to playlist = 在影片或歌曲上開啟選項選單,然後選擇加入播放清單
|
||||
stereo = 立體聲
|
||||
Chapter %d = 第 %d 章
|
||||
Audio %d = 音軌 %d
|
||||
Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. Subtitles: OpenSubtitles.com. = 字型:Selawik、Inter、Noto、DejaVu (OFL)。電視清單:iptv-org。電台:radio-browser.info。字幕:OpenSubtitles.com。
|
||||
15 min = 15 分鐘
|
||||
30 min = 30 分鐘
|
||||
45 min = 45 分鐘
|
||||
60 min = 60 分鐘
|
||||
90 min = 90 分鐘
|
||||
Sign in to the share = 登入共用資料夾
|
||||
%s needs a user name and password. = %s 需要使用者名稱和密碼。
|
||||
A Windows PC takes its own account; a NAS, the user you made on it. = Windows 電腦用它自己的帳戶;NAS 用你在上面建立的使用者。
|
||||
Track %d = 音軌 %d
|
||||
Track = 音軌
|
||||
+16
-4
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the VLC-PS5 app for Linux (the test build: offscreen Vulkan, scripted pad).
|
||||
# bash host/build-app.sh -> $VLC_PS5_WORK/out/vlc-ps5-host
|
||||
# VLCPS5_ASAN=1 bash host/build-app.sh -> out/vlc-ps5-host-asan (AddressSanitizer +
|
||||
# UndefinedBehaviorSanitizer on our code, for stress tests)
|
||||
set -euo pipefail
|
||||
here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
repo=$(dirname "$here")
|
||||
@@ -10,6 +12,13 @@ static=$work/static-host
|
||||
prefix=$work/prefix-host
|
||||
obj=$work/obj-host
|
||||
out=$work/out
|
||||
name=vlc-ps5-host
|
||||
san=()
|
||||
if [[ ${VLCPS5_ASAN:-0} == 1 ]]; then
|
||||
obj=$work/obj-host-asan
|
||||
name=vlc-ps5-host-asan
|
||||
san=(-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=undefined)
|
||||
fi
|
||||
mkdir -p "$obj" "$out"
|
||||
|
||||
bash "$repo/ps5/compile-shaders.sh"
|
||||
@@ -18,14 +27,17 @@ TARGET=host bash "$repo/ps5/gen-static-modules.sh" "$static" >/dev/null
|
||||
inc=(-I"$repo/src" -I"$repo/third_party/imgui" -I"$repo/third_party/volk" -I"$repo/third_party/stb"
|
||||
-I"$vk/.deps/native/radv-release/include" -I"$work/src/vlc-3.0.24/include" -I"$prefix/include")
|
||||
defs=(-DVK_NO_PROTOTYPES -DIMGUI_IMPL_VULKAN_USE_VOLK -DVLCPS5_HOST)
|
||||
cflags=(-O2 -g -Wall -Wno-unused-function "${defs[@]}" "${inc[@]}")
|
||||
cflags=(-O2 -g -Wall -Wno-unused-function "${san[@]}" "${defs[@]}" "${inc[@]}")
|
||||
# the newest of our headers: a file is built again when one changed (a struct
|
||||
# that grew, a default argument) not only when the file itself did
|
||||
newest_h=$(ls -t "$repo"/src/*.h | head -1)
|
||||
|
||||
objs=()
|
||||
compile() { # source compiler [flags...]
|
||||
local src=$1 cc=$2
|
||||
shift 2
|
||||
local o=$obj/$(basename "${src%.*}").o
|
||||
if [[ ! -f $o || $src -nt $o || $repo/src/gen/video_frag.h -nt $o ]]; then
|
||||
if [[ ! -f $o || $src -nt $o || $repo/src/gen/video_frag.h -nt $o || $newest_h -nt $o ]]; then
|
||||
"$cc" "${cflags[@]}" "$@" -c "$src" -o "$o"
|
||||
fi
|
||||
objs+=("$o")
|
||||
@@ -55,5 +67,5 @@ if [[ ${VLCPS5_OWN_IO:-0} == 1 ]]; then
|
||||
objs+=("$obj/libc_missing.o")
|
||||
io_flags=(-Wl,--defsym=readv=xemu_ps5_readv -Wl,--defsym=writev=xemu_ps5_writev)
|
||||
fi
|
||||
g++ -o "$out/vlc-ps5-host" "${objs[@]}" @"$static/link.rsp" -lpthread -ldl "${io_flags[@]}"
|
||||
echo "built $out/vlc-ps5-host"
|
||||
g++ "${san[@]}" -o "$out/$name" "${objs[@]}" @"$static/link.rsp" -lpthread -ldl "${io_flags[@]}"
|
||||
echo "built $out/$name"
|
||||
@@ -204,5 +204,8 @@ fi
|
||||
sha=$(sha256sum "$app/eboot.bin" | cut -c1-12)
|
||||
mkdir -p "$repo/builds"
|
||||
cp "$work/llvm-pie.elf" "$repo/builds/llvm-pie-$sha.elf"
|
||||
# Only the newest three are kept (~140 MB each): enough to read a crash from
|
||||
# the build on the console and the two before it.
|
||||
ls -t "$repo"/builds/llvm-pie-*.elf | tail -n +4 | xargs -r rm -f --
|
||||
printf '==> %s: %s (eboot.bin %s bytes, %s; %s imports)\n' "$title_id" "$app" \
|
||||
"$(stat -c %s "$app/eboot.bin")" "$sha" "$(wc -l < "$app/imports.txt")"
|
||||
+4
-4
@@ -469,13 +469,13 @@ bool gfx_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFla
|
||||
}
|
||||
|
||||
bool gfx_image(uint32_t w, uint32_t h, VkFormat format, VkImageUsageFlags usage, VkImage *image,
|
||||
VkDeviceMemory *memory, VkImageView *view)
|
||||
VkDeviceMemory *memory, VkImageView *view, uint32_t mips)
|
||||
{
|
||||
VkImageCreateInfo ii = { VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
|
||||
ii.imageType = VK_IMAGE_TYPE_2D;
|
||||
ii.format = format;
|
||||
ii.extent = { w, h, 1 };
|
||||
ii.mipLevels = 1;
|
||||
ii.mipLevels = mips;
|
||||
ii.arrayLayers = 1;
|
||||
ii.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
ii.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
@@ -496,7 +496,7 @@ bool gfx_image(uint32_t w, uint32_t h, VkFormat format, VkImageUsageFlags usage,
|
||||
vi.image = *image;
|
||||
vi.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
vi.format = format;
|
||||
vi.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
|
||||
vi.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, mips, 0, 1 };
|
||||
CHECK(vkCreateImageView(gfx.device, &vi, nullptr, view));
|
||||
return true;
|
||||
}
|
||||
@@ -549,6 +549,6 @@ void gfx_barrier(VkCommandBuffer cmd, VkImage image, VkImageLayout from, VkImage
|
||||
b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
b.image = image;
|
||||
b.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
|
||||
b.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, VK_REMAINING_MIP_LEVELS, 0, 1 };
|
||||
vkCmdPipelineBarrier(cmd, src_stage, dst_stage, 0, 0, nullptr, 0, nullptr, 1, &b);
|
||||
}
|
||||
@@ -44,8 +44,9 @@ uint64_t gfx_completed_frame();
|
||||
uint32_t gfx_memory_type(uint32_t type_bits, VkMemoryPropertyFlags flags);
|
||||
bool gfx_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags flags,
|
||||
VkBuffer *buffer, VkDeviceMemory *memory, void **mapped);
|
||||
/* mips: levels made and seen by the view (the caller fills them). */
|
||||
bool gfx_image(uint32_t w, uint32_t h, VkFormat format, VkImageUsageFlags usage, VkImage *image,
|
||||
VkDeviceMemory *memory, VkImageView *view);
|
||||
VkDeviceMemory *memory, VkImageView *view, uint32_t mips = 1);
|
||||
VkShaderModule gfx_shader(const uint32_t *code, size_t size);
|
||||
/* Runs commands now and waits (set-up only). */
|
||||
VkCommandBuffer gfx_one_shot_begin();
|
||||
|
||||
+17
-1
@@ -6,10 +6,13 @@
|
||||
#include "lang.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <fribidi/fribidi.h>
|
||||
@@ -170,7 +173,20 @@ const char *tr(const char *english)
|
||||
if (!t)
|
||||
return english;
|
||||
auto it = t->find(english);
|
||||
return it == t->end() ? english : it->second.c_str();
|
||||
if (it == t->end()) {
|
||||
/* Test aid (the host build with VLCPS5_LANG_MISSES set): each string
|
||||
* drawn that this language lacks, once, so a tour finds them all. */
|
||||
static const bool log_misses = getenv("VLCPS5_LANG_MISSES") != nullptr;
|
||||
if (log_misses) {
|
||||
static std::mutex m;
|
||||
static std::unordered_set<std::string> said;
|
||||
std::lock_guard<std::mutex> g(m);
|
||||
if (said.insert(english).second)
|
||||
fprintf(stderr, "lang-miss: %s\n", english);
|
||||
}
|
||||
return english;
|
||||
}
|
||||
return it->second.c_str();
|
||||
}
|
||||
|
||||
std::string trf(const char *english_format, ...)
|
||||
|
||||
+253
-18
@@ -29,6 +29,7 @@
|
||||
|
||||
#include "backends/imgui_impl_vulkan.h"
|
||||
#include "gfx.h"
|
||||
#include "lang.h"
|
||||
#include "platform.h"
|
||||
#include "player.h"
|
||||
|
||||
@@ -129,6 +130,17 @@ bool has_file(const std::string &path)
|
||||
return stat(path.c_str(), &st) == 0;
|
||||
}
|
||||
|
||||
/* What a scan passed over, for the log: "(folder too deep) 1". */
|
||||
std::map<std::string, int> skipped;
|
||||
int archives_found, others_found;
|
||||
|
||||
/* Folders a drive keeps for the system, never media. */
|
||||
bool system_folder(const char *name)
|
||||
{
|
||||
return !strcmp(name, "System Volume Information") || !strcmp(name, "$RECYCLE.BIN") ||
|
||||
!strcmp(name, "lost+found") || !strcmp(name, "sce_sys") || !strcmp(name, "sce_module");
|
||||
}
|
||||
|
||||
void scan_dir(const std::string &dir, int depth)
|
||||
{
|
||||
/* A disc copied as a folder (VIDEO_TS, BDMV): one item, not its files. */
|
||||
@@ -156,23 +168,32 @@ void scan_dir(const std::string &dir, int depth)
|
||||
continue;
|
||||
std::string path = dir + "/" + e->d_name;
|
||||
struct stat st;
|
||||
if (stat(path.c_str(), &st) != 0)
|
||||
if (stat(path.c_str(), &st) != 0) {
|
||||
skipped["(can't read)"]++;
|
||||
continue;
|
||||
}
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
if (system_folder(e->d_name))
|
||||
continue;
|
||||
if (depth < 4)
|
||||
scan_dir(path, depth + 1);
|
||||
else
|
||||
skipped["(folder too deep)"]++;
|
||||
continue;
|
||||
}
|
||||
const char *dot = strrchr(e->d_name, '.');
|
||||
if (!dot)
|
||||
continue;
|
||||
if (has_ext(dot + 1, playlist_exts)) {
|
||||
dot = e->d_name + strlen(e->d_name); /* no extension: "" */
|
||||
if (!strcasecmp(dot, ".part"))
|
||||
continue; /* a file still arriving from the phone page */
|
||||
if (*dot && has_ext(dot + 1, playlist_exts)) {
|
||||
playlists.push_back({ path, tidy_name(e->d_name), dir });
|
||||
continue;
|
||||
}
|
||||
bool video = has_ext(dot + 1, video_exts), audio = has_ext(dot + 1, audio_exts);
|
||||
bool image = has_ext(dot + 1, image_exts), text = has_ext(dot + 1, text_exts);
|
||||
bool archive = has_ext(dot + 1, archive_exts), iso = !strcasecmp(dot + 1, "iso");
|
||||
const char *x = *dot ? dot + 1 : dot;
|
||||
bool video = has_ext(x, video_exts), audio = has_ext(x, audio_exts);
|
||||
bool image = has_ext(x, image_exts), text = has_ext(x, text_exts);
|
||||
bool archive = has_ext(x, archive_exts), iso = !strcasecmp(x, "iso");
|
||||
if (image) {
|
||||
/* an album's cover file isn't a photo of its own */
|
||||
std::string base = std::string(e->d_name, dot - e->d_name);
|
||||
@@ -181,12 +202,14 @@ void scan_dir(const std::string &dir, int depth)
|
||||
if (base == "cover" || base == "folder" || base == "front" || base.compare(0, 8, "albumart") == 0)
|
||||
continue;
|
||||
}
|
||||
if (!video && !audio && !image && !text && !archive && !iso)
|
||||
continue;
|
||||
/* Everything else shows in Browse too: what VLC can't play says so. */
|
||||
bool other = !video && !audio && !image && !text && !archive && !iso;
|
||||
archives_found += archive;
|
||||
others_found += other;
|
||||
MediaItem m = {};
|
||||
m.path = path;
|
||||
m.name = tidy_name(e->d_name);
|
||||
m.ext = dot + 1;
|
||||
m.name = *dot ? tidy_name(e->d_name) : tidy_name(std::string(e->d_name) + ".x");
|
||||
m.ext = x;
|
||||
for (char &c : m.ext)
|
||||
c = (char)toupper((unsigned char)c);
|
||||
m.folder = dir;
|
||||
@@ -194,6 +217,7 @@ void scan_dir(const std::string &dir, int depth)
|
||||
m.image = image;
|
||||
m.text = text;
|
||||
m.archive = archive;
|
||||
m.other = other;
|
||||
m.disc = iso;
|
||||
m.size = st.st_size;
|
||||
m.mtime = st.st_mtime;
|
||||
@@ -822,7 +846,7 @@ void details_main(std::string path, int64_t size, std::string ext)
|
||||
if (!lang.empty())
|
||||
val += " (" + lang + ")";
|
||||
char label[32];
|
||||
snprintf(label, sizeof(label), "Audio %d", ++audio_no);
|
||||
snprintf(label, sizeof(label), tr("Audio %d"), ++audio_no);
|
||||
d.rows.push_back({ label, val });
|
||||
} else if (t[i]->i_type == libvlc_track_text) {
|
||||
sub_no++;
|
||||
@@ -856,35 +880,86 @@ void apply_resumes()
|
||||
|
||||
/* ---- textures ----------------------------------------------------------------------- */
|
||||
|
||||
/* The next smaller level of a BGRA picture: each pixel the mean of 2x2. */
|
||||
std::vector<uint8_t> half_size(const std::vector<uint8_t> &src, uint32_t w, uint32_t h, uint32_t *nw, uint32_t *nh)
|
||||
{
|
||||
*nw = std::max(1u, w / 2);
|
||||
*nh = std::max(1u, h / 2);
|
||||
std::vector<uint8_t> out((size_t)*nw * *nh * 4);
|
||||
for (uint32_t y = 0; y < *nh; y++) {
|
||||
const uint8_t *r0 = &src[(size_t)std::min(2 * y, h - 1) * w * 4];
|
||||
const uint8_t *r1 = &src[(size_t)std::min(2 * y + 1, h - 1) * w * 4];
|
||||
uint8_t *o = &out[(size_t)y * *nw * 4];
|
||||
for (uint32_t x = 0; x < *nw; x++) {
|
||||
uint32_t x0 = std::min(2 * x, w - 1) * 4, x1 = std::min(2 * x + 1, w - 1) * 4;
|
||||
for (int c = 0; c < 4; c++)
|
||||
o[x * 4 + c] = (uint8_t)((r0[x0 + c] + r0[x1 + c] + r1[x0 + c] + r1[x1 + c] + 2) / 4);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/* A picture as a texture. Thumbnails (and covers) get every smaller level
|
||||
* too: drawn small (a playlist row, a Music cover) a 480-wide picture
|
||||
* sampled once a pixel shimmered; the levels make it smooth at any size.
|
||||
* Big photos are drawn near their size and keep one level. */
|
||||
ImTextureID make_texture(const Finished &t)
|
||||
{
|
||||
if (!thumb_sampler) {
|
||||
VkSamplerCreateInfo si = { VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO };
|
||||
si.magFilter = si.minFilter = VK_FILTER_LINEAR;
|
||||
si.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
|
||||
si.minLod = 0;
|
||||
si.maxLod = VK_LOD_CLAMP_NONE;
|
||||
si.addressModeU = si.addressModeV = si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
vkCreateSampler(gfx.device, &si, nullptr, &thumb_sampler);
|
||||
}
|
||||
/* the levels, one after another in the staging buffer */
|
||||
std::vector<std::vector<uint8_t>> made;
|
||||
made.reserve(12);
|
||||
std::vector<VkBufferImageCopy> regions;
|
||||
VkDeviceSize total = t.bgra.size();
|
||||
regions.push_back({});
|
||||
regions[0].imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 };
|
||||
regions[0].imageExtent = { t.w, t.h, 1 };
|
||||
if (t.w <= 1024 && t.h <= 1024) {
|
||||
uint32_t w = t.w, h = t.h;
|
||||
const std::vector<uint8_t> *src = &t.bgra;
|
||||
while ((w > 1 || h > 1) && regions.size() < 12) {
|
||||
uint32_t nw, nh;
|
||||
made.push_back(half_size(*src, w, h, &nw, &nh));
|
||||
src = &made.back();
|
||||
w = nw;
|
||||
h = nh;
|
||||
VkBufferImageCopy r = {};
|
||||
r.bufferOffset = total;
|
||||
r.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, (uint32_t)regions.size(), 0, 1 };
|
||||
r.imageExtent = { w, h, 1 };
|
||||
regions.push_back(r);
|
||||
total += made.back().size();
|
||||
}
|
||||
}
|
||||
Texture tex;
|
||||
if (!gfx_image(t.w, t.h, VK_FORMAT_B8G8R8A8_UNORM,
|
||||
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, &tex.image,
|
||||
&tex.memory, &tex.view))
|
||||
&tex.memory, &tex.view, (uint32_t)regions.size()))
|
||||
return 0;
|
||||
VkBuffer staging;
|
||||
VkDeviceMemory staging_mem;
|
||||
void *mapped;
|
||||
if (!gfx_buffer(t.bgra.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
|
||||
if (!gfx_buffer(total, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||
&staging, &staging_mem, &mapped))
|
||||
return 0;
|
||||
memcpy(mapped, t.bgra.data(), t.bgra.size());
|
||||
for (size_t i = 0; i < made.size(); i++)
|
||||
memcpy((uint8_t *)mapped + regions[i + 1].bufferOffset, made[i].data(), made[i].size());
|
||||
VkCommandBuffer cmd = gfx_one_shot_begin();
|
||||
gfx_barrier(cmd, tex.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
VkBufferImageCopy region = {};
|
||||
region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 };
|
||||
region.imageExtent = { t.w, t.h, 1 };
|
||||
vkCmdCopyBufferToImage(cmd, staging, tex.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion);
|
||||
vkCmdCopyBufferToImage(cmd, staging, tex.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
(uint32_t)regions.size(), regions.data());
|
||||
gfx_barrier(cmd, tex.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
||||
@@ -984,6 +1059,8 @@ void scan()
|
||||
items.clear();
|
||||
playlists.clear();
|
||||
roots.clear();
|
||||
skipped.clear();
|
||||
archives_found = others_found = 0;
|
||||
roots.push_back(media_dir);
|
||||
scan_dir(media_dir, 0);
|
||||
for (const char *const *r = plat_media_roots(); *r; r++) {
|
||||
@@ -1012,7 +1089,7 @@ void scan()
|
||||
m.favourite = std::find(favourites.begin(), favourites.end(), m.path) != favourites.end();
|
||||
std::vector<Job> todo;
|
||||
for (MediaItem &m : items) {
|
||||
if (m.text || m.archive || m.disc)
|
||||
if (m.text || m.archive || m.disc || m.other)
|
||||
continue; /* no picture to make (a disc's menu is its picture) */
|
||||
auto k = known.find(thumb_file(m));
|
||||
if (k != known.end()) {
|
||||
@@ -1044,6 +1121,11 @@ void scan()
|
||||
last_signature = watch_signature();
|
||||
fprintf(stderr, "library: %zu files in %zu folders, %zu to look at\n", items.size(),
|
||||
roots.size(), todo.size());
|
||||
std::string passed;
|
||||
for (auto &k : skipped)
|
||||
passed += (passed.empty() ? "" : ", ") + k.first + " " + std::to_string(k.second);
|
||||
fprintf(stderr, "library: %d archives, %d other files; passed over: %s\n", archives_found,
|
||||
others_found, passed.empty() ? "nothing" : passed.c_str());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1356,6 +1438,159 @@ std::vector<int> library_playlist_items(const std::string &path)
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ---- playlists made in VLC -------------------------------------------------------- */
|
||||
|
||||
namespace {
|
||||
|
||||
std::string playlist_dir()
|
||||
{
|
||||
return media_dir + "/Playlists";
|
||||
}
|
||||
|
||||
/* A name that can be a file name everywhere (FAT/exFAT drives, Windows). */
|
||||
std::string playlist_file_name(const std::string &name)
|
||||
{
|
||||
std::string n;
|
||||
for (char c : name)
|
||||
n += strchr("/\\:*?\"<>|", c) || (unsigned char)c < 32 ? '-' : c;
|
||||
while (!n.empty() && (n.back() == ' ' || n.back() == '.'))
|
||||
n.pop_back();
|
||||
while (!n.empty() && n[0] == ' ')
|
||||
n.erase(0, 1);
|
||||
return n.substr(0, 120);
|
||||
}
|
||||
|
||||
/* The files of one of ours, in order. */
|
||||
std::vector<std::string> playlist_read(const std::string &path)
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
FILE *f = fopen(path.c_str(), "r");
|
||||
if (!f)
|
||||
return out;
|
||||
char line[4096];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
std::string l = line;
|
||||
while (!l.empty() && (l.back() == '\n' || l.back() == '\r'))
|
||||
l.pop_back();
|
||||
if (l.size() >= 3 && (unsigned char)l[0] == 0xEF)
|
||||
l = l.substr(3);
|
||||
if (!l.empty() && l[0] != '#')
|
||||
out.push_back(l);
|
||||
}
|
||||
fclose(f);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Written whole to a temporary file, then put in place. */
|
||||
bool playlist_write(const std::string &path, const std::vector<std::string> &files)
|
||||
{
|
||||
std::string tmp = path + ".tmp";
|
||||
FILE *f = fopen(tmp.c_str(), "w");
|
||||
if (!f)
|
||||
return false;
|
||||
bool ok = fputs("#EXTM3U\n", f) >= 0;
|
||||
for (const std::string &p : files) {
|
||||
std::string name = p.substr(p.rfind('/') + 1);
|
||||
ok = ok && fprintf(f, "#EXTINF:-1,%s\n%s\n", tidy_name(name).c_str(), p.c_str()) > 0;
|
||||
}
|
||||
ok = fclose(f) == 0 && ok;
|
||||
if (ok)
|
||||
ok = rename(tmp.c_str(), path.c_str()) == 0;
|
||||
if (!ok)
|
||||
unlink(tmp.c_str());
|
||||
return ok;
|
||||
}
|
||||
|
||||
void playlists_sort()
|
||||
{
|
||||
std::sort(playlists.begin(), playlists.end(), [](const PlaylistFile &a, const PlaylistFile &b) {
|
||||
return strcasecmp(a.name.c_str(), b.name.c_str()) < 0;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool library_playlist_ours(const std::string &path)
|
||||
{
|
||||
std::string d = playlist_dir() + "/";
|
||||
return path.compare(0, d.size(), d) == 0 && path.size() > 5 &&
|
||||
!strcasecmp(path.c_str() + path.size() - 5, ".m3u8");
|
||||
}
|
||||
|
||||
std::string library_playlist_create(const std::string &name)
|
||||
{
|
||||
std::string n = playlist_file_name(name);
|
||||
if (n.empty())
|
||||
return "";
|
||||
mkdir(playlist_dir().c_str(), 0777);
|
||||
std::string path = playlist_dir() + "/" + n + ".m3u8";
|
||||
struct stat st;
|
||||
if (stat(path.c_str(), &st) == 0 || !playlist_write(path, {}))
|
||||
return "";
|
||||
playlists.push_back({ path, tidy_name(n + ".m3u8"), playlist_dir() });
|
||||
playlists_sort();
|
||||
return path;
|
||||
}
|
||||
|
||||
bool library_playlist_add(const std::string &playlist, const std::string &file)
|
||||
{
|
||||
std::vector<std::string> files = playlist_read(playlist);
|
||||
if (std::find(files.begin(), files.end(), file) != files.end())
|
||||
return false;
|
||||
files.push_back(file);
|
||||
return playlist_write(playlist, files);
|
||||
}
|
||||
|
||||
bool library_playlist_remove(const std::string &playlist, const std::string &file)
|
||||
{
|
||||
std::vector<std::string> files = playlist_read(playlist);
|
||||
auto it = std::find(files.begin(), files.end(), file);
|
||||
if (it == files.end())
|
||||
return false;
|
||||
files.erase(it);
|
||||
return playlist_write(playlist, files);
|
||||
}
|
||||
|
||||
bool library_playlist_move(const std::string &playlist, const std::string &file, int step)
|
||||
{
|
||||
std::vector<std::string> files = playlist_read(playlist);
|
||||
auto it = std::find(files.begin(), files.end(), file);
|
||||
if (it == files.end())
|
||||
return false;
|
||||
/* to the next one the library has (a file on a drive that's out is
|
||||
* skipped over, so a move always shows) */
|
||||
long i = it - files.begin(), j = i;
|
||||
do
|
||||
j += step;
|
||||
while (j >= 0 && j < (long)files.size() && !library_find(files[j]));
|
||||
if (j < 0 || j >= (long)files.size())
|
||||
return false;
|
||||
std::string moved = files[i];
|
||||
files.erase(files.begin() + i);
|
||||
files.insert(files.begin() + j, moved);
|
||||
return playlist_write(playlist, files);
|
||||
}
|
||||
|
||||
std::string library_playlist_rename(const std::string &playlist, const std::string &name)
|
||||
{
|
||||
std::string n = playlist_file_name(name);
|
||||
if (n.empty())
|
||||
return "";
|
||||
std::string path = playlist_dir() + "/" + n + ".m3u8";
|
||||
struct stat st;
|
||||
if (path == playlist)
|
||||
return path;
|
||||
if (stat(path.c_str(), &st) == 0 || rename(playlist.c_str(), path.c_str()) != 0)
|
||||
return "";
|
||||
for (PlaylistFile &p : playlists)
|
||||
if (p.path == playlist) {
|
||||
p.path = path;
|
||||
p.name = tidy_name(n + ".m3u8");
|
||||
}
|
||||
playlists_sort();
|
||||
return path;
|
||||
}
|
||||
|
||||
std::vector<int64_t> library_bookmarks(const std::string &path)
|
||||
{
|
||||
std::vector<int64_t> out;
|
||||
|
||||
@@ -23,6 +23,7 @@ struct MediaItem {
|
||||
std::string artist, album; /* a song's tags, "" until read (or none) */
|
||||
bool disc; /* a DVD / Blu-ray: an .iso, or a folder with VIDEO_TS or BDMV */
|
||||
bool archive; /* .zip / .rar / .7z: opened like a folder in Browse */
|
||||
bool other; /* any other file: listed in Browse only; X hands it to VLC */
|
||||
int64_t size;
|
||||
int64_t mtime;
|
||||
int64_t length_ms; /* 0 until known */
|
||||
@@ -68,6 +69,19 @@ MediaItem *library_find(const std::string &path);
|
||||
std::vector<PlaylistFile> &library_playlists();
|
||||
/* The library files a playlist names, in its order (others are skipped). */
|
||||
std::vector<int> library_playlist_items(const std::string &path);
|
||||
/* Playlists made in VLC: <media>/Playlists/<name>.m3u8, one absolute path a
|
||||
* line (with #EXTINF names), so VLC on a computer reads them too. Only these
|
||||
* are changed; ones found elsewhere just play. */
|
||||
bool library_playlist_ours(const std::string &path);
|
||||
/* A new empty one; its path, or "" (the name is taken, or no disk). */
|
||||
std::string library_playlist_create(const std::string &name);
|
||||
/* Adds a file at the end; false when it was already in or can't be written. */
|
||||
bool library_playlist_add(const std::string &playlist, const std::string &file);
|
||||
bool library_playlist_remove(const std::string &playlist, const std::string &file);
|
||||
/* Moves a file one place up (-1) or down (+1). */
|
||||
bool library_playlist_move(const std::string &playlist, const std::string &file, int step);
|
||||
/* A new name; the playlist's new path, or "". */
|
||||
std::string library_playlist_rename(const std::string &playlist, const std::string &name);
|
||||
/* Bookmarks inside a file (ms, sorted), remembered in cache/bookmarks.txt. */
|
||||
std::vector<int64_t> library_bookmarks(const std::string &path);
|
||||
void library_add_bookmark(const std::string &path, int64_t ms);
|
||||
|
||||
+9
-5
@@ -722,11 +722,15 @@ void osub_sign_in()
|
||||
}
|
||||
std::string msg, bearer, host;
|
||||
if (a.user.empty()) {
|
||||
/* no account: try the key alone with a small search (the
|
||||
* informational calls answer any key) */
|
||||
Reply r = https("GET", API_HOST, "/api/v1/subtitles?languages=en&query=vlc", api_headers(a, ""), "");
|
||||
finish(r.error.empty() && r.status == 200 ? OSUB_DONE : OSUB_FAILED,
|
||||
r.error.empty() && r.status == 200 ? "The API key works" : server_error(r, "Checking the key"));
|
||||
/* no account: try the key alone. Searches and the informational
|
||||
* calls answer any key, even none; "discover" refuses a bad one
|
||||
* (403, "You cannot consume this service"). */
|
||||
Reply r = https("GET", API_HOST, "/api/v1/discover/popular?languages=en", api_headers(a, ""), "");
|
||||
bool ok = r.error.empty() && r.status == 200;
|
||||
finish(ok ? OSUB_DONE : OSUB_FAILED,
|
||||
ok ? "The API key works"
|
||||
: r.error.empty() && (r.status == 401 || r.status == 403) ? "OpenSubtitles refused this API key"
|
||||
: server_error(r, "Checking the key"));
|
||||
return;
|
||||
}
|
||||
bool ok = sign_in(a, msg, bearer, host);
|
||||
|
||||
+9
-3
@@ -42,6 +42,7 @@
|
||||
#include "gen/video_vert.h"
|
||||
#include "gfx.h"
|
||||
#include "image.h"
|
||||
#include "lang.h"
|
||||
#include "platform.h"
|
||||
#include "prefs.h"
|
||||
|
||||
@@ -583,8 +584,13 @@ void read_info()
|
||||
std::vector<Track> tracks_from(libvlc_track_description_t *list)
|
||||
{
|
||||
std::vector<Track> out;
|
||||
for (libvlc_track_description_t *t = list; t; t = t->p_next)
|
||||
out.push_back({ t->i_id, t->psz_name ? t->psz_name : "Track" });
|
||||
for (libvlc_track_description_t *t = list; t; t = t->p_next) {
|
||||
/* VLC names a track without a title "Track 1": said in the menus' language */
|
||||
std::string name = t->psz_name ? t->psz_name : "";
|
||||
if (name.compare(0, 6, "Track ") == 0 && name.size() > 6 && isdigit((unsigned char)name[6]))
|
||||
name = trf("Track %d", atoi(name.c_str() + 6));
|
||||
out.push_back({ t->i_id, name.empty() ? std::string(tr("Track")) : name });
|
||||
}
|
||||
if (list)
|
||||
libvlc_track_description_list_release(list);
|
||||
return out;
|
||||
@@ -1671,7 +1677,7 @@ std::vector<Chapter> player_chapters()
|
||||
int n = libvlc_media_player_get_full_chapter_descriptions(mp, -1, &d);
|
||||
for (int i = 0; i < n; i++) {
|
||||
char fallback[32];
|
||||
snprintf(fallback, sizeof(fallback), "Chapter %d", i + 1);
|
||||
snprintf(fallback, sizeof(fallback), tr("Chapter %d"), i + 1);
|
||||
chapters_cache.push_back({ d[i]->i_time_offset,
|
||||
d[i]->psz_name && *d[i]->psz_name ? d[i]->psz_name : fallback });
|
||||
}
|
||||
|
||||
@@ -160,6 +160,19 @@ std::string vlc_message(const std::string &m)
|
||||
{
|
||||
if (m.compare(0, 29, "VLC is unable to open the MRL") == 0)
|
||||
return "VLC couldn't open this address: it may be offline, or need a login";
|
||||
/* the SMB login (VLC's smb2 module): who asks, then what to type */
|
||||
if (m == "SMB authentication required")
|
||||
return "Sign in to the share";
|
||||
if (m.compare(0, 14, "The computer (") == 0 && m.find(')') != std::string::npos) {
|
||||
std::string host = m.substr(14, m.find(')') - 14);
|
||||
bool more = m.find("Please provide") != std::string::npos;
|
||||
return trf("%s needs a user name and password.", host.c_str()) +
|
||||
(more ? std::string(" ") + tr("A Windows PC takes its own account; a NAS, the user you made on it.") : "");
|
||||
}
|
||||
if (m.compare(0, 14, "Please provide") == 0)
|
||||
return "A Windows PC takes its own account; a NAS, the user you made on it.";
|
||||
if (m.compare(0, 6, "Track ") == 0 && m.size() > 6 && isdigit((unsigned char)m[6]))
|
||||
return trf("Track %d", atoi(m.c_str() + 6));
|
||||
return m;
|
||||
}
|
||||
|
||||
@@ -424,6 +437,8 @@ void ci_file(float cx, float cy, float s, int kind)
|
||||
tri(bx, by + s * 0.16f, bx + s * 0.12f, by + s * 0.04f, bx + s * 0.24f, by + s * 0.16f, IM_COL32(50, 125, 55, 255));
|
||||
stroke(bx - s * 0.24f, by - s * 0.2f, s * 0.48f, s * 0.36f, IM_COL32(90, 96, 106, 255), 0, 1);
|
||||
break;
|
||||
case 5: /* any other file: a blank page */
|
||||
break;
|
||||
default:
|
||||
for (int i = 0; i < 4; i++)
|
||||
rect(bx - s * 0.2f, by - s * 0.22f + i * s * 0.1f, s * 0.4f, s * 0.035f, IM_COL32(160, 166, 176, 255));
|
||||
@@ -806,8 +821,8 @@ bool hit(uint32_t b)
|
||||
|
||||
/* ---- state -------------------------------------------------------------------- */
|
||||
|
||||
enum Tab { HOME, VIDEOS, MUSIC, BROWSE, TABS };
|
||||
const char *const tab_names[TABS] = { "Home", "Videos", "Music", "Browse" };
|
||||
enum Tab { HOME, VIDEOS, MUSIC, PLAYLISTS, BROWSE, TABS };
|
||||
const char *const tab_names[TABS] = { "Home", "Videos", "Music", "Playlists", "Browse" };
|
||||
|
||||
enum Screen { LIBRARY, PLAYER, VIEWER };
|
||||
Screen screen = LIBRARY;
|
||||
@@ -862,7 +877,7 @@ uint64_t hash_id(const std::string &s, uint64_t salt)
|
||||
|
||||
/* Modals. */
|
||||
enum Modal { NONE, TEXT_VIEW, OPTIONS, RESUME, INFO, TRACKS, DETAILS, SETTINGS, SPEAKERS, PHONE, SEARCH, LINK, CONFIRM_DELETE,
|
||||
ADD_SHARE, LOGIN, QUESTION, OSUB_SETUP, OSUB_RESULTS };
|
||||
ADD_SHARE, LOGIN, QUESTION, OSUB_SETUP, OSUB_RESULTS, PL_PICK, PL_NAME };
|
||||
Modal modal = NONE;
|
||||
float modal_anim;
|
||||
int modal_focus;
|
||||
@@ -967,7 +982,7 @@ void rebuild_lists()
|
||||
music_list.clear();
|
||||
pictures_list.clear();
|
||||
for (int i = 0; i < (int)items.size(); i++)
|
||||
if (!items[i].text && !items[i].archive)
|
||||
if (!items[i].text && !items[i].archive && !items[i].other)
|
||||
(items[i].image ? pictures_list : items[i].audio ? music_list : videos_list).push_back(i);
|
||||
home_rows.clear();
|
||||
Row cont = { "Continue watching", {} }, recent = { "Recently added", {} },
|
||||
@@ -1053,7 +1068,8 @@ void rebuild_browse()
|
||||
for (int i = 0; i < (int)items.size(); i++) {
|
||||
const std::string &f = items[i].folder;
|
||||
if (f == browse_dir) {
|
||||
browse_entries.push_back({ items[i].name + "." + items[i].ext, items[i].path, false, i, false });
|
||||
browse_entries.push_back({ items[i].ext.empty() ? items[i].name : items[i].name + "." + items[i].ext,
|
||||
items[i].path, false, i, false });
|
||||
} else if (f.compare(0, browse_dir.size() + 1, browse_dir + "/") == 0) {
|
||||
std::string sub = f.substr(browse_dir.size() + 1);
|
||||
sub = sub.substr(0, sub.find('/'));
|
||||
@@ -1149,6 +1165,12 @@ void open_item(int item, const std::vector<int> &list)
|
||||
open_archive(m);
|
||||
return;
|
||||
}
|
||||
if (m.other) {
|
||||
/* listed so Browse shows everything, but not something VLC plays:
|
||||
* no player screen, just the note */
|
||||
toast(trf("Couldn't play %s", (m.ext.empty() ? m.name : m.name + "." + m.ext).c_str()), 2.5);
|
||||
return;
|
||||
}
|
||||
if (m.resume_ms > 0) {
|
||||
modal = RESUME;
|
||||
modal_focus = 0;
|
||||
@@ -1302,7 +1324,13 @@ void tab_icon(int i, float cx, float cy, float s, ImU32 col)
|
||||
line(cx + s * 0.45f, cy + s * 0.22f, cx + s * 0.45f, cy - s * 0.48f, col, t);
|
||||
line(cx - s * 0.11f, cy - s * 0.4f, cx + s * 0.45f, cy - s * 0.48f, col, t);
|
||||
break;
|
||||
case 3: /* folder: tab on top, open front */
|
||||
case 3: /* playlists: lines and a play mark */
|
||||
line(cx - s * 0.5f, cy - s * 0.36f, cx + s * 0.5f, cy - s * 0.36f, col, t);
|
||||
line(cx - s * 0.5f, cy - s * 0.02f, cx + s * 0.08f, cy - s * 0.02f, col, t);
|
||||
line(cx - s * 0.5f, cy + s * 0.32f, cx + s * 0.08f, cy + s * 0.32f, col, t);
|
||||
tri(cx + s * 0.24f, cy - s * 0.06f, cx + s * 0.24f, cy + s * 0.42f, cx + s * 0.56f, cy + s * 0.18f, col);
|
||||
break;
|
||||
case 4: /* folder: tab on top, open front */
|
||||
dl->PathLineTo(P(cx - s * 0.5f, cy + s * 0.38f));
|
||||
dl->PathLineTo(P(cx - s * 0.5f, cy - s * 0.38f));
|
||||
dl->PathLineTo(P(cx - s * 0.12f, cy - s * 0.38f));
|
||||
@@ -1491,7 +1519,7 @@ void video_card_classic(float x, float y, float w, int item, float f)
|
||||
rect(x, y + h - 4, w * frac, 4, C_ORANGE);
|
||||
}
|
||||
text(f_reg, 22, x, y + h + 12, C_TEXT, m.name.c_str(), 0, w);
|
||||
std::string meta = m.audio && !m.artist.empty() ? m.artist : m.ext + (m.image ? " image" : m.audio ? " audio" : " video");
|
||||
std::string meta = m.audio && !m.artist.empty() ? m.artist : trf(m.image ? "%s image" : m.audio ? "%s audio" : "%s video", m.ext.c_str());
|
||||
if (m.resume_ms > 0)
|
||||
meta += ", " + trf("%s left", format_time(m.length_ms - m.resume_ms).c_str());
|
||||
text(f_reg, 18, x, y + h + 42, C_FAINT, meta.c_str(), 0, w);
|
||||
@@ -1638,7 +1666,7 @@ int focused_item()
|
||||
case VIDEOS:
|
||||
return videos_list.empty() ? -1 : videos_list[std::min(video_focus, (int)videos_list.size() - 1)];
|
||||
case MUSIC: {
|
||||
int np = (int)library_playlists().size();
|
||||
int np = 0; /* playlists have their own tab */
|
||||
if (music_focus < np || music_list.empty())
|
||||
return -1;
|
||||
return music_list[std::min(music_focus - np, (int)music_list.size() - 1)];
|
||||
@@ -1687,7 +1715,7 @@ void library_actions()
|
||||
int item = focused_item();
|
||||
if (item < 0)
|
||||
return;
|
||||
if (hit(PAD_TRIANGLE)) {
|
||||
if (hit(PAD_TRIANGLE) && !L()[item].other) { /* no favourites for files VLC doesn't play */
|
||||
std::string path = L()[item].path;
|
||||
bool on = library_toggle_favourite(path);
|
||||
rebuild_lists();
|
||||
@@ -1830,7 +1858,7 @@ void list_row(float x, float y, float w, float h, float f, bool even)
|
||||
void tab_music()
|
||||
{
|
||||
auto &pls = library_playlists();
|
||||
int np = (int)pls.size(), ns = (int)music_list.size(), n = np + ns;
|
||||
int np = 0, ns = (int)music_list.size(), n = np + ns; /* playlists have their own tab */
|
||||
if (n == 0) {
|
||||
empty_state();
|
||||
return;
|
||||
@@ -1945,6 +1973,379 @@ void tab_music()
|
||||
dl->PopClipRect();
|
||||
}
|
||||
|
||||
/* ---- Playlists ------------------------------------------------------------------- */
|
||||
|
||||
/* The list of playlists ("New playlist" first), or one opened: its files. */
|
||||
int pl_focus, pl_item_focus;
|
||||
float pl_scroll, pl_item_scroll;
|
||||
std::string pl_open; /* the playlist being looked at, "" for the list */
|
||||
std::vector<int> pl_items; /* its files that are in the library */
|
||||
double pl_items_at = -100; /* read again now and then (drives come and go) */
|
||||
std::string pl_add_file; /* PL_PICK / PL_NAME: the file to put in */
|
||||
std::string pl_rename; /* PL_NAME: the playlist renamed, "" for a new one */
|
||||
void icon_plus_circle(float cx, float cy, float s, ImU32 col);
|
||||
void open_playlist_name(const std::string &rename);
|
||||
|
||||
const PlaylistFile *playlist_by_path(const std::string &path)
|
||||
{
|
||||
for (const PlaylistFile &p : library_playlists())
|
||||
if (p.path == path)
|
||||
return &p;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void pl_reload()
|
||||
{
|
||||
pl_items = library_playlist_items(pl_open);
|
||||
pl_items_at = now;
|
||||
if (pl_item_focus >= (int)pl_items.size())
|
||||
pl_item_focus = std::max(0, (int)pl_items.size() - 1);
|
||||
}
|
||||
|
||||
/* "3 videos · 1:24:10", for a list of library files. */
|
||||
std::string pl_summary(const std::vector<int> &list)
|
||||
{
|
||||
int64_t total = 0;
|
||||
for (int i : list)
|
||||
total += std::max<int64_t>(0, L()[i].length_ms);
|
||||
std::string out = list.size() == 1 ? std::string(tr("1 item")) : trf("%d items", (int)list.size());
|
||||
if (total > 0)
|
||||
out += " \xC2\xB7 " + format_time(total);
|
||||
return out;
|
||||
}
|
||||
|
||||
float text_wrapped(ImFont *f, float size, float x, float y, ImU32 col, const std::string &s_in, float max_w, int max_lines);
|
||||
|
||||
/* A playlist's files, looked up again every few seconds (for covers and counts). */
|
||||
const std::vector<int> &playlist_items_cached(const std::string &path)
|
||||
{
|
||||
struct Cached {
|
||||
double at;
|
||||
std::vector<int> items;
|
||||
};
|
||||
static std::unordered_map<std::string, Cached> cache;
|
||||
Cached &c = cache[path];
|
||||
if (c.at == 0 || now - c.at > 3) {
|
||||
c.items = library_playlist_items(path);
|
||||
c.at = now;
|
||||
}
|
||||
return c.items;
|
||||
}
|
||||
|
||||
/* A picture covering (x, y, w, h): the thumbnail cropped to that shape. */
|
||||
void thumb_cover(const MediaItem &m, float x, float y, float w, float h, float round, ImDrawFlags corners)
|
||||
{
|
||||
float ta = m.thumb_aspect > 0 ? m.thumb_aspect : (m.audio ? 1.0f : 16.0f / 9), ca = w / h;
|
||||
float u0 = 0, v0 = 0, u1 = 1, v1 = 1;
|
||||
if (ta > ca) {
|
||||
u0 = (1 - ca / ta) / 2;
|
||||
u1 = 1 - u0;
|
||||
} else {
|
||||
v0 = (1 - ta / ca) / 2;
|
||||
v1 = 1 - v0;
|
||||
}
|
||||
dl->AddImageRounded(m.thumb, P(x, y), P(x + w, y + h), ImVec2(u0, v0), ImVec2(u1, v1), IM_COL32_WHITE,
|
||||
round * S, corners);
|
||||
}
|
||||
|
||||
/* A playlist's cover, as VLC 4 makes it: its first file's picture, or the
|
||||
* first four in a 2x2 mosaic, each a 16:9 picture of its own (no squeezing). */
|
||||
void playlist_cover(const std::string &path, float x, float y, float w, float h, float round)
|
||||
{
|
||||
std::vector<int> pics;
|
||||
for (int i : playlist_items_cached(path))
|
||||
if (i < (int)L().size() && L()[i].thumb && pics.size() < 4)
|
||||
pics.push_back(i);
|
||||
if (pics.empty()) {
|
||||
dl->AddRectFilledMultiColor(P(x, y), P(x + w, y + h), tint_for(path, 0.30f), tint_for(path + "#", 0.22f),
|
||||
tint_for(path, 0.12f), tint_for(path + "#", 0.16f));
|
||||
if (round > 0)
|
||||
stroke(x, y, w, h, C_BG, round, 4); /* round off the gradient's corners */
|
||||
icon_list(x + w / 2, y + h / 2, h * 0.34f, IM_COL32(255, 255, 255, 170));
|
||||
return;
|
||||
}
|
||||
if (pics.size() < 4) {
|
||||
thumb_cover(L()[pics[0]], x, y, w, h, round, ImDrawFlags_RoundCornersAll);
|
||||
return;
|
||||
}
|
||||
const float gap = 3;
|
||||
float cw = (w - gap) / 2, ch = (h - gap) / 2;
|
||||
static const ImDrawFlags corner[4] = { ImDrawFlags_RoundCornersTopLeft, ImDrawFlags_RoundCornersTopRight,
|
||||
ImDrawFlags_RoundCornersBottomLeft, ImDrawFlags_RoundCornersBottomRight };
|
||||
rect(x, y, w, h, IM_COL32(0, 0, 0, 255), round);
|
||||
for (int k = 0; k < 4; k++)
|
||||
thumb_cover(L()[pics[k]], x + (k % 2) * (cw + gap), y + (k / 2) * (ch + gap), cw, ch, round, corner[k]);
|
||||
}
|
||||
|
||||
/* A playlist card, the size of a video card: the cover, the name, what's in
|
||||
* it. path "" is the "New playlist" card. */
|
||||
void playlist_card(float x, float y, float w, const PlaylistFile *pl, float f)
|
||||
{
|
||||
float h = w * 9 / 16;
|
||||
std::string sub;
|
||||
if (pl) {
|
||||
const std::vector<int> &items = playlist_items_cached(pl->path);
|
||||
sub = library_playlist_ours(pl->path) ? pl_summary(items)
|
||||
: std::string(tr("Playlist")) + " \xC2\xB7 " + pl->folder.substr(pl->folder.rfind('/') + 1);
|
||||
}
|
||||
const char *name = pl ? pl->name.c_str() : tr("New playlist");
|
||||
if (classic_look) {
|
||||
sel_box(x - 12, y - 12, w + 24, h + 96, f);
|
||||
rect(x + 3, y + 4, w, h, IM_COL32(0, 0, 0, 110));
|
||||
if (pl) {
|
||||
playlist_cover(pl->path, x, y, w, h, 0);
|
||||
} else {
|
||||
vgrad(x, y, w, h, IM_COL32(52, 56, 64, 255), IM_COL32(30, 32, 37, 255));
|
||||
ci_file(x + w / 2, y + h / 2, h * 0.5f, 2);
|
||||
circle(x + w / 2 + h * 0.16f, y + h / 2 + h * 0.16f, h * 0.12f, C_ORANGE);
|
||||
rect(x + w / 2 + h * 0.16f - h * 0.07f, y + h / 2 + h * 0.16f - h * 0.015f, h * 0.14f, h * 0.03f, IM_COL32_WHITE);
|
||||
rect(x + w / 2 + h * 0.16f - h * 0.015f, y + h / 2 + h * 0.16f - h * 0.07f, h * 0.03f, h * 0.14f, IM_COL32_WHITE);
|
||||
}
|
||||
stroke(x, y, w, h, IM_COL32(255, 255, 255, 46), 0, 1);
|
||||
text(f_reg, 22, x, y + h + 12, C_TEXT, name, 0, w);
|
||||
if (pl)
|
||||
text(f_reg, 18, x, y + h + 42, C_FAINT, sub.c_str(), 0, w);
|
||||
else /* long in some languages: two lines */
|
||||
text_wrapped(f_reg, 18, x, y + h + 42, C_FAINT, "Then add videos and songs from anywhere with the Options menu", w, 2);
|
||||
return;
|
||||
}
|
||||
float grow = 1 + 0.07f * ease(f);
|
||||
float cw = w * grow, ch = h * grow;
|
||||
float cx = x - (cw - w) / 2, cy = y - (ch - h) / 2 - 6 * f;
|
||||
glow(cx, cy + 10, cw, ch, 16, IM_COL32(0, 0, 0, 255), 22);
|
||||
if (f > 0.01f)
|
||||
glow(cx, cy, cw, ch, 16, alpha(C_ORANGE, f * 0.9f), 18);
|
||||
if (pl) {
|
||||
playlist_cover(pl->path, cx, cy, cw, ch, 16);
|
||||
/* how many, bottom right, like a video's length */
|
||||
const std::vector<int> &items = playlist_items_cached(pl->path);
|
||||
std::string count = std::to_string(items.size());
|
||||
float tw = text_size(f_semi, 18, count.c_str()).x;
|
||||
rect(cx + cw - tw - 54, cy + ch - 40, tw + 44, 28, IM_COL32(0, 0, 0, 170), 8);
|
||||
icon_list(cx + cw - tw - 38, cy + ch - 26, 16, C_TEXT);
|
||||
text(f_semi, 18, cx + cw - tw - 18, cy + ch - 36, C_TEXT, count.c_str());
|
||||
} else {
|
||||
rect(cx, cy, cw, ch, IM_COL32(255, 255, 255, 12), 16);
|
||||
if (f > 0.01f)
|
||||
rect(cx, cy, cw, ch, alpha(C_ORANGE, 0.10f * f), 16); /* a light tint when chosen */
|
||||
stroke(cx, cy, cw, ch, alpha(C_ORANGE, 0.35f + 0.45f * f), 16, 2);
|
||||
icon_plus_circle(cx + cw / 2, cy + ch / 2, ch * 0.32f, mix(C_DIM, C_ORANGE, 0.5f + 0.5f * f));
|
||||
}
|
||||
if (f > 0.01f)
|
||||
stroke(cx - 2, cy - 2, cw + 4, ch + 4, alpha(IM_COL32(255, 255, 255, 255), f), 18, 3);
|
||||
text(f_semi, 22, x, y + h + 16 + 4 * f, mix(C_DIM, C_TEXT, 0.6f + 0.4f * f), name, 0, w);
|
||||
if (pl)
|
||||
text(f_reg, 18, x, y + h + 46 + 4 * f, C_FAINT, sub.c_str(), 0, w);
|
||||
else /* long in some languages: two lines */
|
||||
text_wrapped(f_reg, 18, x, y + h + 46 + 4 * f, C_FAINT, "Then add videos and songs from anywhere with the Options menu", w, 2);
|
||||
}
|
||||
|
||||
void tab_playlists()
|
||||
{
|
||||
auto &pls = library_playlists();
|
||||
if (!pl_open.empty() && !playlist_by_path(pl_open))
|
||||
pl_open.clear(); /* deleted, or its drive went */
|
||||
if (pl_open.empty()) {
|
||||
/* "New playlist", then the playlists, as cards in rows of four */
|
||||
const int cols = 4;
|
||||
const float card_w = 396, gap_x = (1920 - 192 - cols * card_w) / (cols - 1), row_h = 330;
|
||||
int n = 1 + (int)pls.size();
|
||||
if (pl_focus >= n)
|
||||
pl_focus = n - 1;
|
||||
if (hit(PAD_LEFT) && pl_focus % cols > 0)
|
||||
pl_focus--;
|
||||
if (hit(PAD_RIGHT) && pl_focus % cols < cols - 1 && pl_focus + 1 < n)
|
||||
pl_focus++;
|
||||
if (hit(PAD_UP) && pl_focus >= cols)
|
||||
pl_focus -= cols;
|
||||
if (hit(PAD_DOWN) && pl_focus + cols < n)
|
||||
pl_focus += cols;
|
||||
if (hit(PAD_CROSS)) {
|
||||
if (pl_focus == 0) {
|
||||
pl_add_file.clear();
|
||||
open_playlist_name("");
|
||||
} else {
|
||||
pl_open = pls[pl_focus - 1].path;
|
||||
pl_item_focus = 0;
|
||||
pl_item_scroll = 0;
|
||||
pl_reload();
|
||||
}
|
||||
} else if (hit(PAD_TRIANGLE) && pl_focus > 0) {
|
||||
play_playlist(pls[pl_focus - 1].path);
|
||||
} else if (hit(PAD_SQUARE) && pl_focus > 0 && library_playlist_ours(pls[pl_focus - 1].path)) {
|
||||
ask_delete(pls[pl_focus - 1].path, NONE);
|
||||
}
|
||||
int frow = pl_focus / cols;
|
||||
pl_scroll = approach(pl_scroll, std::max(0.0f, 206 + (frow + 1) * row_h - 1000), 10);
|
||||
if (classic_look) {
|
||||
classic_heading(96, 140, "Playlists", pls.size());
|
||||
} else {
|
||||
text(f_bold, 30, 96, 140, C_TEXT, "Playlists");
|
||||
if (!pls.empty())
|
||||
text(f_semi, 22, 104 + text_size(f_bold, 30, "Playlists").x, 147, C_FAINT, std::to_string(pls.size()).c_str());
|
||||
}
|
||||
dl->PushClipRect(P(0, 186), P(1920, 1080), true);
|
||||
for (int i = 0; i < n; i++) {
|
||||
float x = 96 + (i % cols) * (card_w + gap_x);
|
||||
float y = 206 + (i / cols) * row_h - pl_scroll;
|
||||
if (y > 1080 || y + row_h < 100)
|
||||
continue;
|
||||
float f = focus_anim(hash_id(i ? pls[i - 1].path : "+new", 230), i == pl_focus && modal == NONE);
|
||||
playlist_card(x, y, card_w, i ? &pls[i - 1] : nullptr, f);
|
||||
}
|
||||
dl->PopClipRect();
|
||||
return;
|
||||
}
|
||||
|
||||
/* One playlist open. */
|
||||
const PlaylistFile *pl = playlist_by_path(pl_open);
|
||||
bool ours = library_playlist_ours(pl_open);
|
||||
if (now - pl_items_at > 2)
|
||||
pl_reload();
|
||||
int n = (int)pl_items.size();
|
||||
if (hit(PAD_CIRCLE)) {
|
||||
for (int i = 0; i < (int)pls.size(); i++)
|
||||
if (pls[i].path == pl_open)
|
||||
pl_focus = i + 1; /* back on the one just left */
|
||||
pl_open.clear();
|
||||
return;
|
||||
}
|
||||
if (n > 0) {
|
||||
if (hit(PAD_UP) && pl_item_focus > 0)
|
||||
pl_item_focus--;
|
||||
if (hit(PAD_DOWN) && pl_item_focus + 1 < n)
|
||||
pl_item_focus++;
|
||||
std::string cur = L()[pl_items[pl_item_focus]].path;
|
||||
if (hit(PAD_CROSS)) {
|
||||
std::vector<int> list = pl_items;
|
||||
open_item(list[pl_item_focus], list);
|
||||
from_playlist = true; /* the next file follows by itself */
|
||||
return;
|
||||
} else if (hit(PAD_TRIANGLE)) {
|
||||
play_playlist(pl_open);
|
||||
return;
|
||||
} else if (ours && hit(PAD_SQUARE)) {
|
||||
if (library_playlist_remove(pl_open, cur))
|
||||
toast(trf("Removed from %s", pl->name.c_str()), 1.8);
|
||||
pl_reload();
|
||||
} else if (ours && (hit(PAD_L2) || hit(PAD_R2))) {
|
||||
int step = hit(PAD_R2) ? 1 : -1;
|
||||
if (library_playlist_move(pl_open, cur, step)) {
|
||||
pl_reload();
|
||||
for (int i = 0; i < (int)pl_items.size(); i++)
|
||||
if (L()[pl_items[i]].path == cur)
|
||||
pl_item_focus = i; /* the selection goes with it */
|
||||
}
|
||||
}
|
||||
}
|
||||
n = (int)pl_items.size();
|
||||
std::string sum = pl_summary(pl_items);
|
||||
if (classic_look) {
|
||||
const float top = 190, bottom = 996, rh = 48, head_h = 50;
|
||||
const float col_type = 1100, col_len = 1792;
|
||||
classic_columns(top - 50, { { "Name", 168, 0 }, { "Type", col_type, 0 }, { "Length", col_len, 1 } });
|
||||
float view = bottom - top;
|
||||
float want = std::max(0.0f, std::min(head_h + pl_item_focus * rh + rh / 2 - view * 0.45f,
|
||||
std::max(0.0f, head_h + n * rh - view + 10)));
|
||||
pl_item_scroll = approach(pl_item_scroll, want, 14);
|
||||
dl->PushClipRect(P(0, top - 6), P(1920, bottom), true);
|
||||
classic_heading(112, top - pl_item_scroll + 16, pl->name.c_str(), (size_t)n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
float y = top + head_h + i * rh - pl_item_scroll;
|
||||
if (y > bottom || y + rh < top - 60)
|
||||
continue;
|
||||
MediaItem &m = L()[pl_items[i]];
|
||||
float f = focus_anim(hash_id(m.path, 240), i == pl_item_focus && modal == NONE, 16);
|
||||
sel_box(100, y + 2, 1720, rh - 4, f);
|
||||
text(f_reg, 21, 128, y + 13, C_FAINT, std::to_string(i + 1).c_str(), 0.5f);
|
||||
ci_file(176, y + rh / 2, 30, m.audio ? 1 : 0);
|
||||
text(f_reg, 23, 206, y + 11, C_TEXT, m.name.c_str(), 0, col_type - 240);
|
||||
text(f_reg, 21, col_type, y + 13, C_DIM, trf(m.audio ? "%s audio" : "%s video", m.ext.c_str()).c_str());
|
||||
if (m.length_ms > 0)
|
||||
text(f_reg, 21, col_len, y + 13, C_DIM, format_time(m.length_ms).c_str(), 1);
|
||||
}
|
||||
dl->PopClipRect();
|
||||
if (n == 0) {
|
||||
text(f_semi, 26, 960, 470, C_TEXT, "This playlist is empty", 0.5f);
|
||||
text(f_reg, 21, 960, 512, C_DIM, "Open the Options menu on a video or song and choose Add to playlist", 0.5f);
|
||||
}
|
||||
status_text = sum;
|
||||
return;
|
||||
}
|
||||
/* Rows with a picture the same 16:9 size for every file (160x90):
|
||||
* a video fills it, a song's square cover sits in the middle of it. */
|
||||
const float row_h = 114, pic_w = 160, pic_h = 90;
|
||||
pl_item_scroll = approach(pl_item_scroll, std::max(0.0f, pl_item_focus * row_h - 330), 12);
|
||||
text(f_bold, 30, 96, 128, C_TEXT, pl->name.c_str(), 0, 1300);
|
||||
text(f_reg, 20, 96, 168, C_FAINT, sum.c_str());
|
||||
dl->PushClipRect(P(0, 206), P(1920, 1080), true);
|
||||
for (int i = 0; i < n; i++) {
|
||||
float y = 216 + i * row_h - pl_item_scroll;
|
||||
if (y > 1080 || y + row_h < 150)
|
||||
continue;
|
||||
MediaItem &m = L()[pl_items[i]];
|
||||
float f = focus_anim(hash_id(m.path, 240), i == pl_item_focus && modal == NONE);
|
||||
list_row(96, y, 1728, row_h - 12, f, i % 2 == 0);
|
||||
float cy = y + (row_h - 12) / 2;
|
||||
text(f_semi, 22, 136, cy - 14, mix(C_FAINT, C_ORANGE, f), std::to_string(i + 1).c_str(), 0.5f);
|
||||
float px = 172, py = cy - pic_h / 2;
|
||||
float ta = m.thumb_aspect > 0 ? m.thumb_aspect : (m.audio ? 1.0f : 16.0f / 9);
|
||||
if (m.thumb && (ta < 1.6f || ta > 2.0f)) {
|
||||
/* not 16:9 (a song's square cover, 4:3, a phone's tall video):
|
||||
* the whole picture, in the middle of a dark frame */
|
||||
float fw = std::min(pic_w, pic_h * ta), fh = fw / ta;
|
||||
rect(px, py, pic_w, pic_h, IM_COL32(255, 255, 255, 10), 10);
|
||||
thumb_cover(m, px + (pic_w - fw) / 2, py + (pic_h - fh) / 2, fw, fh, 8, ImDrawFlags_RoundCornersAll);
|
||||
} else if (m.thumb) {
|
||||
thumb_cover(m, px, py, pic_w, pic_h, 10, ImDrawFlags_RoundCornersAll); /* 16:9, near enough: fills it */
|
||||
} else {
|
||||
dl->AddRectFilledMultiColor(P(px, py), P(px + pic_w, py + pic_h), tint_for(m.name, 0.30f),
|
||||
tint_for(m.name + "#", 0.22f), tint_for(m.name, 0.12f),
|
||||
tint_for(m.name + "#", 0.16f));
|
||||
m.audio ? icon_note(px + pic_w / 2, cy, 34, IM_COL32(255, 255, 255, 170))
|
||||
: icon_film(px + pic_w / 2, cy, 38, IM_COL32(255, 255, 255, 150));
|
||||
}
|
||||
if (f > 0.01f)
|
||||
stroke(px - 2, py - 2, pic_w + 4, pic_h + 4, alpha(IM_COL32(255, 255, 255, 255), f), 12, 2);
|
||||
float tx = px + pic_w + 28;
|
||||
text(f_semi, 26, tx, cy - 32, mix(C_DIM, C_TEXT, 0.7f + 0.3f * f), m.name.c_str(), 0, 1300 - tx);
|
||||
std::string sub = !m.artist.empty() ? m.artist : m.ext + " \xC2\xB7 " + m.folder.substr(m.folder.rfind('/') + 1);
|
||||
text(f_reg, 19, tx, cy + 4, C_FAINT, sub.c_str(), 0, 1300 - tx);
|
||||
if (m.length_ms > 0)
|
||||
text(f_semi, 22, 1792, cy - 14, C_DIM, format_time(m.length_ms).c_str(), 1);
|
||||
}
|
||||
dl->PopClipRect();
|
||||
if (n == 0) {
|
||||
icon_list(960, 470, 70, C_FAINT);
|
||||
text(f_semi, 28, 960, 540, C_TEXT, "This playlist is empty", 0.5f);
|
||||
text(f_reg, 21, 960, 584, C_DIM, "Open the Options menu on a video or song and choose Add to playlist", 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
/* The hint bar's buttons on the Playlists tab. */
|
||||
std::vector<std::pair<uint32_t, const char *>> playlist_hints()
|
||||
{
|
||||
auto &pls = library_playlists();
|
||||
if (pl_open.empty()) {
|
||||
if (pl_focus == 0)
|
||||
return { { PAD_CROSS, "Create" } };
|
||||
std::vector<std::pair<uint32_t, const char *>> h = { { PAD_CROSS, "Open" }, { PAD_TRIANGLE, "Play" } };
|
||||
if (pl_focus - 1 < (int)pls.size() && library_playlist_ours(pls[pl_focus - 1].path))
|
||||
h.push_back({ PAD_SQUARE, "Delete" });
|
||||
return h;
|
||||
}
|
||||
std::vector<std::pair<uint32_t, const char *>> h;
|
||||
if (!pl_items.empty()) {
|
||||
h = { { PAD_CROSS, "Play" }, { PAD_TRIANGLE, "Play all" } };
|
||||
if (library_playlist_ours(pl_open)) {
|
||||
h.push_back({ PAD_SQUARE, "Remove" });
|
||||
h.push_back({ PAD_R2, "Move" });
|
||||
}
|
||||
}
|
||||
h.push_back({ PAD_CIRCLE, "Back" });
|
||||
return h;
|
||||
}
|
||||
|
||||
void start_stream(const std::string &url, const std::vector<std::string> &options = {}, bool remember = true,
|
||||
const std::string &name = "", libvlc_media_t *media = nullptr);
|
||||
void open_add_share();
|
||||
@@ -2092,6 +2493,8 @@ void classic_label(const BrowseEntry &e, std::string *name, std::string *type, i
|
||||
*type = m.ext == "ISO" ? "Disc image (ISO)" : m.ext == "DVD" ? "DVD (folder)" : "Blu-ray (folder)";
|
||||
} else if (e.item >= 0 && L()[e.item].archive) {
|
||||
*type = trf("Compressed (%s)", L()[e.item].ext.c_str());
|
||||
} else if (e.item >= 0 && L()[e.item].other) {
|
||||
*type = L()[e.item].ext.empty() ? std::string(tr("File")) : trf("%s file", L()[e.item].ext.c_str());
|
||||
} else if (e.item >= 0) {
|
||||
const MediaItem &m = L()[e.item];
|
||||
*type = m.text ? (m.ext == "NFO" || m.ext == "DIZ" ? trf("Release info (%s)", m.ext.c_str()) : std::string(tr("Text document")))
|
||||
@@ -2119,6 +2522,8 @@ void classic_icon(const BrowseEntry &e, float cx, float cy, float s)
|
||||
ci_disc(cx, cy, s);
|
||||
else if (L()[e.item].archive)
|
||||
ci_zip(cx, cy, s);
|
||||
else if (L()[e.item].other)
|
||||
ci_file(cx, cy, s, 5);
|
||||
else
|
||||
ci_file(cx, cy, s, L()[e.item].text ? 3 : L()[e.item].image ? 4 : L()[e.item].audio ? 1 : 0);
|
||||
}
|
||||
@@ -2441,7 +2846,7 @@ void tab_browse()
|
||||
}
|
||||
std::vector<int> list;
|
||||
for (const BrowseEntry &b : browse_entries)
|
||||
if (b.item >= 0)
|
||||
if (b.item >= 0 && (!L()[b.item].other || b.item == e.item)) /* "next" skips other files */
|
||||
list.push_back(b.item);
|
||||
open_item(e.item, list);
|
||||
}
|
||||
@@ -2465,14 +2870,14 @@ void tab_browse()
|
||||
return;
|
||||
}
|
||||
/* Breadcrumb: "Media › Movies", not the raw path. */
|
||||
std::string crumb = "Sources";
|
||||
std::string crumb = tr("Sources");
|
||||
bool in_archive = !net_path.empty() && net_path[0].first.compare(0, 7, "file://") == 0;
|
||||
if (!net_path.empty() && !in_archive)
|
||||
crumb = "Network";
|
||||
crumb = tr("Network");
|
||||
for (const std::string &r : library_roots()) {
|
||||
if ((!net_path.empty() && !in_archive) || browse_dir.compare(0, r.size(), r) != 0)
|
||||
continue;
|
||||
crumb = r == library_media_dir() ? "Media" : r.substr(r.rfind('/') + 1);
|
||||
crumb = r == library_media_dir() ? std::string(tr("Media")) : r.substr(r.rfind('/') + 1);
|
||||
std::string rest = browse_dir.substr(r.size());
|
||||
size_t p;
|
||||
while (!rest.empty() && (p = rest.find('/', 1)) != std::string::npos) {
|
||||
@@ -2518,6 +2923,8 @@ void tab_browse()
|
||||
icon_list(146, y + 33, 30, C_DIM);
|
||||
else if (L()[e.item].archive)
|
||||
icon_folder(146, y + 33, 34, C_DIM);
|
||||
else if (L()[e.item].other)
|
||||
icon_list(146, y + 33, 30, C_FAINT);
|
||||
else
|
||||
icon_film(146, y + 33, 34, C_DIM);
|
||||
/* A file on a share: its name, the format in a badge (like local files). */
|
||||
@@ -2559,7 +2966,7 @@ void tab_browse()
|
||||
text(f_reg, 24, 960, 476, C_DIM, trf("Connecting to %s\xE2\x80\xA6", tr(net_path.back().second.c_str())).c_str(), 0.5f, 1400);
|
||||
} else if (!net_path.empty() && net_list_state() == NET_FAILED) {
|
||||
text(f_semi, 26, 960, 400, C_TEXT, "Couldn't open this place", 0.5f);
|
||||
text(f_reg, 22, 960, 446, C_DIM, net_list_error().c_str(), 0.5f, 1500);
|
||||
text(f_reg, 22, 960, 446, C_DIM, vlc_message(net_list_error()).c_str(), 0.5f, 1500);
|
||||
} else if (!n) {
|
||||
text(f_reg, 24, 960, 400, C_DIM, net_path.empty() ? "Nothing here" : "This folder is empty", 0.5f);
|
||||
}
|
||||
@@ -3083,6 +3490,157 @@ void modal_add_share()
|
||||
hints(1824, 1040, { { PAD_SQUARE, "Delete" }, { PAD_TRIANGLE, "Space" }, { PAD_R2, "Add" }, { PAD_CIRCLE, "Close" } }, a);
|
||||
}
|
||||
|
||||
/* "Add to playlist": "New playlist" first, then the ones made in VLC. */
|
||||
std::string trimmed(std::string s);
|
||||
|
||||
void open_playlist_pick(const std::string &file)
|
||||
{
|
||||
pl_add_file = file;
|
||||
modal = PL_PICK;
|
||||
modal_focus = 0;
|
||||
modal_anim = 0;
|
||||
}
|
||||
|
||||
void open_playlist_name(const std::string &rename)
|
||||
{
|
||||
pl_rename = rename;
|
||||
const PlaylistFile *pl = rename.empty() ? nullptr : playlist_by_path(rename);
|
||||
kb_open(pl ? pl->name : "", false);
|
||||
in_list = false;
|
||||
modal = PL_NAME;
|
||||
modal_anim = 0;
|
||||
}
|
||||
|
||||
void add_to_playlist(const std::string &playlist)
|
||||
{
|
||||
const PlaylistFile *pl = playlist_by_path(playlist);
|
||||
std::string name = pl ? pl->name : "";
|
||||
if (library_playlist_add(playlist, pl_add_file))
|
||||
toast(trf("Added to %s", name.c_str()), 2);
|
||||
else
|
||||
toast(trf("Already in %s", name.c_str()), 2);
|
||||
if (playlist == pl_open)
|
||||
pl_reload();
|
||||
}
|
||||
|
||||
void modal_pl_pick()
|
||||
{
|
||||
std::vector<const PlaylistFile *> ours;
|
||||
for (const PlaylistFile &p : library_playlists())
|
||||
if (library_playlist_ours(p.path))
|
||||
ours.push_back(&p);
|
||||
int n = 1 + (int)ours.size();
|
||||
if (modal_focus >= n)
|
||||
modal_focus = n - 1;
|
||||
if (hit(PAD_UP) && n > 0)
|
||||
modal_focus = (modal_focus + n - 1) % n;
|
||||
if (hit(PAD_DOWN) && n > 0)
|
||||
modal_focus = (modal_focus + 1) % n;
|
||||
if (hit(PAD_CIRCLE)) {
|
||||
modal = NONE;
|
||||
return;
|
||||
}
|
||||
MediaItem *m = library_find(pl_add_file);
|
||||
float a = ease(modal_anim);
|
||||
dim_screen(a);
|
||||
const float w = 640, row = 76;
|
||||
int shown = std::min(n, 7);
|
||||
float h = 150 + shown * row + 70;
|
||||
float x = 960 - w / 2, y = 540 - h / 2 + (1 - a) * 40;
|
||||
panel(x, y, w, h, a);
|
||||
text(f_bold, 30, x + 40, y + 30, alpha(C_TEXT, a), "Add to playlist");
|
||||
if (m)
|
||||
text(f_reg, 21, x + 40, y + 74, alpha(C_DIM, a), m->name.c_str(), 0, w - 80);
|
||||
int first = std::max(0, std::min(modal_focus - shown / 2, n - shown));
|
||||
for (int k = 0; k < shown; k++) {
|
||||
int i = first + k;
|
||||
float ry = y + 120 + k * row;
|
||||
const char *label = i == 0 ? tr("New playlist") : ours[i - 1]->name.c_str();
|
||||
float f = focus_anim(hash_id(i == 0 ? "+new" : ours[i - 1]->path, 950), i == modal_focus, 14);
|
||||
if (classic_look) {
|
||||
sel_box(x + 20, ry, w - 40, row - 10, f * a);
|
||||
} else if (f > 0.01f) {
|
||||
glow(x + 20, ry, w - 40, row - 10, 16, alpha(C_ORANGE, 0.35f * f * a), 12, 6);
|
||||
rect(x + 20, ry, w - 40, row - 10, alpha(C_CARD_HI, f * a), 16);
|
||||
}
|
||||
ImU32 ic = alpha(f > 0.5f ? IM_COL32(255, 255, 255, 255) : C_DIM, a);
|
||||
if (!classic_look)
|
||||
rect(x + 36, ry + 9, 48, 48, alpha(mix(IM_COL32(255, 255, 255, 14), C_ORANGE, 0.85f * f), a), 13);
|
||||
if (i == 0)
|
||||
icon_plus_circle(x + 60, ry + 33, 28, ic);
|
||||
else
|
||||
icon_list(x + 60, ry + 33, 26, ic);
|
||||
text(f_semi, 24, x + 104, ry + 19, alpha(mix(C_DIM, C_TEXT, 0.55f + 0.45f * f), a), label, 0, w - 140);
|
||||
}
|
||||
hints(x + w - 40, y + h - 40, { { PAD_CROSS, "Add" }, { PAD_CIRCLE, "Cancel" } }, a);
|
||||
if (hit(PAD_CROSS)) {
|
||||
if (modal_focus == 0) {
|
||||
open_playlist_name("");
|
||||
} else {
|
||||
add_to_playlist(ours[modal_focus - 1]->path);
|
||||
modal = NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* A playlist's name, typed: a new one (and the file waiting to go in), or a
|
||||
* new name for one. */
|
||||
void modal_pl_name()
|
||||
{
|
||||
float a = ease(modal_anim);
|
||||
screen_backdrop(a);
|
||||
const float x = 260, w = 1400;
|
||||
text(f_bold, 36, x, classic_look ? 32 : 64, alpha(C_TEXT, a), pl_rename.empty() ? "New playlist" : "Rename playlist");
|
||||
text_box(x, 128, w, "Name the playlist", a, true);
|
||||
MediaItem *m = pl_add_file.empty() ? nullptr : library_find(pl_add_file);
|
||||
if (m)
|
||||
text(f_reg, 22, x, 248, alpha(C_DIM, a), trf("%s goes in first", m->name.c_str()).c_str(), 0, w);
|
||||
else
|
||||
text(f_reg, 22, x, 248, alpha(C_DIM, a), "Saved in the media folder's Playlists, as a file VLC on a computer opens too", 0, w);
|
||||
bool done = kb_frame(x, 640, w, true, a, nullptr);
|
||||
if (done) {
|
||||
std::string t = trimmed(kb.text);
|
||||
if (t.empty()) {
|
||||
toast("Type a name", 2.5);
|
||||
} else if (pl_rename.empty()) {
|
||||
std::string path = library_playlist_create(t);
|
||||
if (path.empty()) {
|
||||
toast("A playlist with this name is already there, or the disk can't be written", 3);
|
||||
} else {
|
||||
modal = NONE;
|
||||
if (!pl_add_file.empty()) {
|
||||
add_to_playlist(path);
|
||||
} else {
|
||||
toast(trf("Made %s", t.c_str()), 2);
|
||||
tab = PLAYLISTS; /* and on it */
|
||||
pl_open.clear();
|
||||
for (int i = 0; i < (int)library_playlists().size(); i++)
|
||||
if (library_playlists()[i].path == path)
|
||||
pl_focus = i + 1;
|
||||
}
|
||||
rebuild_browse();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
std::string path = library_playlist_rename(pl_rename, t);
|
||||
if (path.empty()) {
|
||||
toast("A playlist with this name is already there, or the disk can't be written", 3);
|
||||
} else {
|
||||
if (pl_open == pl_rename)
|
||||
pl_open = path;
|
||||
modal = NONE;
|
||||
rebuild_browse();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hit(PAD_CIRCLE)) {
|
||||
modal = NONE;
|
||||
return;
|
||||
}
|
||||
hints(1824, 1040, { { PAD_SQUARE, "Delete" }, { PAD_TRIANGLE, "Space" }, { PAD_R2, "Save" }, { PAD_CIRCLE, "Cancel" } }, a);
|
||||
}
|
||||
|
||||
/* Text cut into lines that fit max_w; returns the height used. */
|
||||
float text_wrapped(ImFont *f, float size, float x, float y, ImU32 col, const std::string &s_in, float max_w, int max_lines)
|
||||
{
|
||||
@@ -3175,8 +3733,8 @@ void modal_login()
|
||||
float a = ease(modal_anim);
|
||||
screen_backdrop(a);
|
||||
const float x = 260, w = 1400;
|
||||
text(f_bold, 36, x, classic_look ? 32 : 56, alpha(C_TEXT, a), ask.title.empty() ? "Sign in" : ask.title.c_str(), 0, w);
|
||||
text_wrapped(f_reg, 22, x, 112, alpha(C_DIM, a), ask.text, w, 3);
|
||||
text(f_bold, 36, x, classic_look ? 32 : 56, alpha(C_TEXT, a), ask.title.empty() ? "Sign in" : vlc_message(ask.title).c_str(), 0, w);
|
||||
text_wrapped(f_reg, 22, x, 112, alpha(C_DIM, a), vlc_message(ask.text), w, 3);
|
||||
/* the field being typed in holds kb.text */
|
||||
(login_field == 0 ? login_user : login_pass) = kb.text;
|
||||
field_box(x, 210, (w - 40) / 2, "User name", login_user, false, login_field == 0, a);
|
||||
@@ -3237,8 +3795,8 @@ void modal_question()
|
||||
dim_screen(a);
|
||||
float w = 1100, h = 440, x = 960 - w / 2, y = 540 - h / 2 + (1 - a) * 40;
|
||||
panel(x, y, w, h, a);
|
||||
text(f_bold, 30, x + 48, y + 40, alpha(C_TEXT, a), ask.title.c_str(), 0, w - 96);
|
||||
text_wrapped(f_reg, 21, x + 48, y + 96, alpha(C_DIM, a), ask.text, w - 96, 6);
|
||||
text(f_bold, 30, x + 48, y + 40, alpha(C_TEXT, a), vlc_message(ask.title).c_str(), 0, w - 96);
|
||||
text_wrapped(f_reg, 21, x + 48, y + 96, alpha(C_DIM, a), vlc_message(ask.text), w - 96, 6);
|
||||
float bw = (w - 96 - (nb - 1) * 24) / nb;
|
||||
for (int i = 0; i < nb; i++) {
|
||||
float bx = x + 48 + i * (bw + 24), by = y + h - 120;
|
||||
@@ -3702,7 +4260,7 @@ void modal_text_screen()
|
||||
} else {
|
||||
std::vector<int> list;
|
||||
for (const Found &x2 : found)
|
||||
if (x2.item >= 0)
|
||||
if (x2.item >= 0 && (!L()[x2.item].other || x2.item == r.item))
|
||||
list.push_back(x2.item);
|
||||
open_item(r.item, list);
|
||||
}
|
||||
@@ -3793,15 +4351,14 @@ void modal_about()
|
||||
rect(tx, cy + 14, tw, 1, alpha(IM_COL32(255, 255, 255, 24), a));
|
||||
text(f_semi, 19, tx, cy + 34, alpha(C_DIM, a), "Built with");
|
||||
text_wrapped(f_reg, 18, tx, cy + 64, alpha(C_FAINT, a),
|
||||
"FFmpeg 8.1.2, dav1d 1.5.4, libmatroska 1.7.2, libebml 1.4.6, libdvbpsi 1.3.3, libass 0.17.5, "
|
||||
std::string("FFmpeg 8.1.2, dav1d 1.5.4, libmatroska 1.7.2, libebml 1.4.6, libdvbpsi 1.3.3, libass 0.17.5, "
|
||||
"FreeType 2.13.3, HarfBuzz 10.4.0, FriBidi 1.0.16, GnuTLS 3.8.13, Nettle 3.10.2, GMP 6.3.0, "
|
||||
"libsmb2 6.1, libupnp 1.14.31, libxml2 2.15.3, libdvdread 6.1.3, libdvdnav 6.1.1, libbluray 1.4.1, "
|
||||
"libarchive 3.8.9, zlib 1.3.1, Mesa RADV (Vulkan), Dear ImGui 1.92.9, stb_image. "
|
||||
"Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. "
|
||||
"Subtitles: OpenSubtitles.com.",
|
||||
"libarchive 3.8.9, zlib 1.3.1, Mesa RADV (Vulkan), Dear ImGui 1.92.9, stb_image. ") +
|
||||
tr("Fonts: Selawik, Inter, Noto, DejaVu (OFL). TV lists: iptv-org. Radio: radio-browser.info. Subtitles: OpenSubtitles.com."),
|
||||
tw, 6);
|
||||
text(f_reg, 17, tx, y + h - 52, alpha(C_FAINT, a),
|
||||
"GPLv2+. An unofficial port, not affiliated with VideoLAN or Sony.", 0, tw);
|
||||
"GPL-3.0. An unofficial port, not affiliated with VideoLAN or Sony.", 0, tw);
|
||||
hints(x + w - 40, y + h - 40, { { PAD_CIRCLE, "Close" } }, a);
|
||||
if (hit(PAD_CIRCLE) || hit(PAD_CROSS)) {
|
||||
modal = info_back;
|
||||
@@ -3812,7 +4369,8 @@ void modal_about()
|
||||
|
||||
/* Options (the OPTIONS button): a few actions, each with its icon. The
|
||||
* choices live in Settings. */
|
||||
enum { OI_SETTINGS, OI_RESCAN, OI_RESUME, OI_TRACKS, OI_STOP, OI_PHONE, OI_SEARCH, OI_LINK };
|
||||
enum { OI_SETTINGS, OI_RESCAN, OI_RESUME, OI_TRACKS, OI_STOP, OI_PHONE, OI_SEARCH, OI_LINK, OI_ADD_PL, OI_RENAME_PL,
|
||||
OI_DELETE_PL };
|
||||
|
||||
void icon_search(float cx, float cy, float s, ImU32 col)
|
||||
{
|
||||
@@ -3847,6 +4405,8 @@ void option_icon(int icon, float cx, float cy, float s, ImU32 col)
|
||||
case OI_PHONE: icon_phone(cx, cy, s, col); break;
|
||||
case OI_SEARCH: icon_search(cx, cy, s, col); break;
|
||||
case OI_LINK: icon_link(cx, cy, s, col); break;
|
||||
case OI_ADD_PL: icon_plus_circle(cx, cy, s * 0.95f, col); break;
|
||||
case OI_RENAME_PL: icon_list(cx, cy, s * 0.9f, col); break;
|
||||
default: rect(cx - s * 0.26f, cy - s * 0.26f, s * 0.52f, s * 0.52f, col, s * 0.08f); break;
|
||||
}
|
||||
}
|
||||
@@ -3861,11 +4421,29 @@ void modal_options()
|
||||
std::vector<Item> items;
|
||||
if (in_player)
|
||||
items = { { "Resume", OI_RESUME }, { "Audio, subtitles and more", OI_TRACKS },
|
||||
{ "Phone remote", OI_PHONE }, { "Settings", OI_SETTINGS }, { "Stop playback", OI_STOP } };
|
||||
{ "Send files from a phone or PC", OI_PHONE }, { "Settings", OI_SETTINGS }, { "Stop playback", OI_STOP } };
|
||||
else
|
||||
items = { { "Search", OI_SEARCH }, { "Open a network link", OI_LINK },
|
||||
{ "Send files from a phone", OI_PHONE }, { "Settings", OI_SETTINGS },
|
||||
{ "Send files from a phone or PC", OI_PHONE }, { "Settings", OI_SETTINGS },
|
||||
{ "Rescan media", OI_RESCAN } };
|
||||
/* the file in front of you: into a playlist */
|
||||
int target = in_player ? current_item() : focused_item();
|
||||
std::string pl_target; /* Playlists tab: the one opened or selected */
|
||||
if (!in_player && tab == PLAYLISTS) {
|
||||
if (!pl_open.empty())
|
||||
pl_target = pl_open;
|
||||
else if (pl_focus > 0 && pl_focus - 1 < (int)library_playlists().size())
|
||||
pl_target = library_playlists()[pl_focus - 1].path;
|
||||
if (!library_playlist_ours(pl_target))
|
||||
pl_target.clear();
|
||||
}
|
||||
if (target >= 0 && !L()[target].other && !L()[target].text && !L()[target].archive && !L()[target].image &&
|
||||
!is_link(L()[target].path))
|
||||
items.insert(items.begin() + 1, { "Add to playlist", OI_ADD_PL });
|
||||
if (!pl_target.empty()) {
|
||||
items.insert(items.begin(), { "Delete playlist", OI_DELETE_PL });
|
||||
items.insert(items.begin(), { "Rename playlist", OI_RENAME_PL });
|
||||
}
|
||||
int n = (int)items.size();
|
||||
if (modal_focus >= n)
|
||||
modal_focus = n - 1;
|
||||
@@ -3937,6 +4515,16 @@ void modal_options()
|
||||
case OI_STOP:
|
||||
leave_player(false);
|
||||
break;
|
||||
case OI_ADD_PL:
|
||||
open_playlist_pick(L()[target].path);
|
||||
break;
|
||||
case OI_RENAME_PL:
|
||||
pl_add_file.clear();
|
||||
open_playlist_name(pl_target);
|
||||
break;
|
||||
case OI_DELETE_PL:
|
||||
ask_delete(pl_target, NONE);
|
||||
break;
|
||||
default: /* Resume */
|
||||
break;
|
||||
}
|
||||
@@ -3996,7 +4584,7 @@ const char *const st_help[ST_COUNT] = {
|
||||
"VLC for PS5 v" VLC_PS5_VERSION ", VLC media player 3.0.24",
|
||||
"Starts VLC again: it then sees drives plugged in since (when sandboxed)",
|
||||
"Version, credits and licences",
|
||||
"The page that sends files and works as a remote, on your home network",
|
||||
"The page that sends files and takes your OpenSubtitles key, on your home network",
|
||||
"Classic: panels and folders like a desktop player. Modern: black, VLC 4 style. VLC restarts",
|
||||
"Your own free OpenSubtitles API key (and account), here or from the phone page",
|
||||
"The language of VLC's menus",
|
||||
@@ -4535,7 +5123,7 @@ void modal_phone()
|
||||
panel(x, y, w, h, a);
|
||||
rect(x + 48, y + 48, 64, 64, alpha(C_ORANGE, a), 16);
|
||||
icon_phone(x + 80, y + 80, 40, alpha(IM_COL32(255, 255, 255, 255), a));
|
||||
text(f_bold, 32, x + 136, y + 62, alpha(C_TEXT, a), "Send files from your phone");
|
||||
text(f_bold, 32, x + 136, y + 62, alpha(C_TEXT, a), "Send files from your phone or PC");
|
||||
if (!web_running()) {
|
||||
text(f_reg, 24, x + 48, y + 160, alpha(C_DIM, a), "Phone access is off. Turn it on in Settings > System.", 0, w - 96);
|
||||
} else if (url.empty()) {
|
||||
@@ -4545,7 +5133,7 @@ void modal_phone()
|
||||
rect(x + 48, y + 200, w - 96, 96, alpha(C_CARD_HI, a), 18);
|
||||
text(f_bold, 44, x + w / 2, y + 222, alpha(C_ORANGE, a), url.c_str(), 0.5f, w - 140);
|
||||
static const char *const what[] = { "Send videos, music and subtitles to the PS5",
|
||||
"Use it as a remote while something plays" };
|
||||
"Add your OpenSubtitles API key" };
|
||||
for (int i = 0; i < 2; i++) {
|
||||
float ly = y + 330 + i * 42;
|
||||
circle(x + 58, ly + 14, 5, alpha(C_ORANGE, a));
|
||||
@@ -4565,61 +5153,21 @@ void modal_phone()
|
||||
hints(x + w - 40, y + h - 36, { { PAD_CIRCLE, "Close" } }, a);
|
||||
}
|
||||
|
||||
/* The page's view of the player, its buttons, and files that arrived. */
|
||||
/* The page's subtitle-download card, and what arrived from it. */
|
||||
void web_frame()
|
||||
{
|
||||
static double next_status;
|
||||
if (now >= next_status) {
|
||||
next_status = now + 0.25;
|
||||
WebStatus st = {};
|
||||
st.playing = screen == PLAYER && !playing_path.empty();
|
||||
if (st.playing) {
|
||||
st.paused = player_paused() || ended;
|
||||
st.title = playing_name;
|
||||
st.time_ms = player_time();
|
||||
st.length_ms = player_length();
|
||||
}
|
||||
st.volume = player_volume();
|
||||
st.loop = loop_on();
|
||||
web_set_status(st);
|
||||
}
|
||||
WebCommand c;
|
||||
while (web_next_command(&c)) {
|
||||
if (screen != PLAYER)
|
||||
continue;
|
||||
switch (c.kind) {
|
||||
case WEB_TOGGLE:
|
||||
if (ended) {
|
||||
int item = current_item();
|
||||
if (item >= 0)
|
||||
restart_playing(item);
|
||||
} else {
|
||||
player_toggle_pause();
|
||||
pulse_anim = 1;
|
||||
pulse_paused = !player_paused();
|
||||
}
|
||||
break;
|
||||
case WEB_JUMP:
|
||||
player_seek(player_time() + c.value);
|
||||
last_seek_at = now;
|
||||
osd_until = now + 3;
|
||||
break;
|
||||
case WEB_SEEK:
|
||||
player_seek(c.value);
|
||||
last_seek_at = now;
|
||||
osd_until = now + 3;
|
||||
break;
|
||||
case WEB_NEXT: play_neighbour(1); break;
|
||||
case WEB_PREV: play_neighbour(-1); break;
|
||||
case WEB_VOLUME: {
|
||||
player_set_volume(player_volume() + (int)c.value);
|
||||
char msg[32];
|
||||
snprintf(msg, sizeof(msg), tr("Volume %d%%"), player_volume());
|
||||
toast(msg, 1);
|
||||
break;
|
||||
}
|
||||
case WEB_STOP: leave_player(false); break;
|
||||
}
|
||||
next_status = now + 0.5;
|
||||
WebOsubInfo info;
|
||||
info.has_key = osub_has_key();
|
||||
/* the key check as the TV saw it (osub_state() hands a result over
|
||||
* once: asking here would take it from the TV) */
|
||||
info.checking = os_checking;
|
||||
info.failed = !os_checking && !os_status.empty() && !os_status_ok;
|
||||
info.user = pref_str("osub_user", "");
|
||||
info.message = os_checking ? "" : os_status;
|
||||
web_set_osub_info(info);
|
||||
}
|
||||
std::string ok_key, ok_user, ok_pass;
|
||||
if (web_take_opensubtitles(&ok_key, &ok_user, &ok_pass)) {
|
||||
@@ -4628,13 +5176,13 @@ void web_frame()
|
||||
pref_set("osub_pass", ok_pass);
|
||||
if (modal == OSUB_SETUP)
|
||||
open_osub_setup(os_back); /* the form shows what arrived */
|
||||
toast("OpenSubtitles key received from your phone", 3);
|
||||
toast("OpenSubtitles key received from your phone or PC", 3);
|
||||
osub_check();
|
||||
}
|
||||
if (modal != OSUB_SETUP && modal != OSUB_RESULTS)
|
||||
osub_check_tick();
|
||||
if (web_take_finished()) {
|
||||
toast("A file arrived from your phone", 2.5);
|
||||
toast("A file arrived from your phone or PC", 2.5);
|
||||
if (screen != PLAYER) {
|
||||
library_rescan();
|
||||
rebuild_lists();
|
||||
@@ -6323,7 +6871,7 @@ void player_screen()
|
||||
if (!v.audio_codec.empty()) {
|
||||
meta += (meta.empty() ? "" : " \xC2\xB7 ") + v.audio_codec;
|
||||
if (v.audio_channels)
|
||||
meta += v.audio_channels == 6 ? " 5.1" : v.audio_channels == 8 ? " 7.1" : v.audio_channels == 2 ? " stereo" : "";
|
||||
meta += v.audio_channels == 6 ? " 5.1" : v.audio_channels == 8 ? " 7.1" : v.audio_channels == 2 ? " " + std::string(tr("stereo")) : "";
|
||||
}
|
||||
{
|
||||
std::vector<Chapter> ch = player_chapters();
|
||||
@@ -7064,6 +7612,7 @@ void ui_frame(const PadState &pad, float frame_dt)
|
||||
case HOME: tab_home(); break;
|
||||
case VIDEOS: tab_videos(); break;
|
||||
case MUSIC: tab_music(); break;
|
||||
case PLAYLISTS: tab_playlists(); break;
|
||||
case BROWSE: tab_browse(); break;
|
||||
default: break;
|
||||
}
|
||||
@@ -7074,7 +7623,9 @@ void ui_frame(const PadState &pad, float frame_dt)
|
||||
(browse_entries[browse_focus].dir || browse_entries[browse_focus].net == NET_ADD);
|
||||
std::vector<std::pair<uint32_t, const char *>> h = { { PAD_CROSS, open_kind ? "Open" : "Play" } };
|
||||
int fi = focused_item();
|
||||
if (fi >= 0) {
|
||||
if (fi >= 0 && L()[fi].other) {
|
||||
h = { { PAD_SQUARE, "Details" } };
|
||||
} else if (fi >= 0) {
|
||||
h.push_back({ PAD_TRIANGLE, L()[fi].favourite ? "Unfavourite" : "Favourite" });
|
||||
h.push_back({ PAD_SQUARE, "Details" });
|
||||
} else if (tab == BROWSE && browse_focus < (int)browse_entries.size() &&
|
||||
@@ -7087,10 +7638,15 @@ void ui_frame(const PadState &pad, float frame_dt)
|
||||
}
|
||||
if (tab == BROWSE && (!browse_dir.empty() || !net_path.empty()))
|
||||
h.push_back({ PAD_CIRCLE, "Back" });
|
||||
if (tab == PLAYLISTS)
|
||||
h = playlist_hints();
|
||||
h.push_back({ PAD_R3, "Search" });
|
||||
h.push_back({ PAD_OPTIONS, "Options" });
|
||||
if (classic_look) {
|
||||
if (tab != BROWSE) {
|
||||
if (tab == PLAYLISTS) {
|
||||
if (pl_open.empty())
|
||||
status_text = trf("%d playlists", (int)library_playlists().size());
|
||||
} else if (tab != BROWSE) {
|
||||
char st[64];
|
||||
if (tab == VIDEOS)
|
||||
snprintf(st, sizeof(st), tr("%zu videos"), videos_list.size());
|
||||
@@ -7128,6 +7684,8 @@ void ui_frame(const PadState &pad, float frame_dt)
|
||||
case RESUME: modal_resume(); break;
|
||||
case INFO: modal_info(); break;
|
||||
case DETAILS: modal_details(); break;
|
||||
case PL_PICK: modal_pl_pick(); break;
|
||||
case PL_NAME: modal_pl_name(); break;
|
||||
case SETTINGS: modal_settings(); break;
|
||||
case SPEAKERS: modal_speakers(); break;
|
||||
case PHONE: modal_phone(); break;
|
||||
|
||||
+67
-44
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* VLC-PS5's web page server: plain HTTP/1.1 on port 8080 for the local
|
||||
* network. One thread listens; each connection gets its own (at most 8), so a
|
||||
* long upload doesn't hold up the remote. Uploads stream to "<name>.part" and
|
||||
* long upload doesn't hold up the rest of the page. Uploads stream to "<name>.part" and
|
||||
* are renamed when complete, so the library never sees half a file.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <arpa/inet.h>
|
||||
#include <ctype.h>
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
@@ -41,8 +41,7 @@ const int MAX_CONNECTIONS = 8;
|
||||
|
||||
std::mutex lock;
|
||||
std::vector<WebPlace> places;
|
||||
WebStatus status;
|
||||
std::deque<WebCommand> commands;
|
||||
WebOsubInfo osub_info;
|
||||
WebUpload upload;
|
||||
bool finished;
|
||||
/* an OpenSubtitles key and account sent from the page */
|
||||
@@ -249,6 +248,56 @@ void handle_upload(int fd, const std::string &query, int64_t length, std::string
|
||||
respond(fd, ok ? 200 : 500, "text/plain", ok ? "ok" : "The upload stopped: connection or disk full");
|
||||
}
|
||||
|
||||
/* A file from the PS5 to the phone or computer: streamed from the disk in
|
||||
* 1 MiB pieces, so any size goes (a 40 GB film too), as an attachment so
|
||||
* the browser saves it instead of playing it. */
|
||||
void handle_download(int fd, const std::string &query)
|
||||
{
|
||||
std::string dir, name = query_value(query, "name");
|
||||
if (!place_path(query_value(query, "dir"), &dir) || !safe_name(name)) {
|
||||
respond(fd, 400, "text/plain", "That name or folder can't be used");
|
||||
return;
|
||||
}
|
||||
std::string path = dir + "/" + name;
|
||||
int in = open(path.c_str(), O_RDONLY);
|
||||
struct stat st;
|
||||
if (in < 0 || fstat(in, &st) != 0 || !S_ISREG(st.st_mode)) {
|
||||
if (in >= 0)
|
||||
close(in);
|
||||
respond(fd, 404, "text/plain", "not found");
|
||||
return;
|
||||
}
|
||||
/* the name for the browser: plain letters as they are, the rest %-coded */
|
||||
std::string coded;
|
||||
for (unsigned char c : name) {
|
||||
char hex[4];
|
||||
snprintf(hex, sizeof(hex), "%%%02X", c);
|
||||
coded += isalnum(c) || strchr("-._~", c) ? std::string(1, (char)c) : std::string(hex);
|
||||
}
|
||||
char head[1024];
|
||||
int n = snprintf(head, sizeof(head),
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: %lld\r\n"
|
||||
"Content-Disposition: attachment; filename*=UTF-8''%s\r\n"
|
||||
"Cache-Control: no-store\r\nConnection: close\r\n\r\n",
|
||||
(long long)st.st_size, coded.c_str());
|
||||
struct timeval tv = { 60, 0 }; /* a phone that stops reading lets go of the thread */
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
bool ok = n > 0 && n < (int)sizeof(head) && write_all(fd, head, (size_t)n);
|
||||
std::vector<char> buf(1 << 20);
|
||||
int64_t sent = 0;
|
||||
while (ok) {
|
||||
ssize_t r = read(in, buf.data(), buf.size());
|
||||
if (r < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (r <= 0)
|
||||
break;
|
||||
ok = write_all(fd, buf.data(), (size_t)r);
|
||||
sent += r;
|
||||
}
|
||||
close(in);
|
||||
fprintf(stderr, "web: %s %s\n", name.c_str(), sent == st.st_size ? "sent" : "stopped (connection)");
|
||||
}
|
||||
|
||||
void handle_files(int fd, const std::string &query)
|
||||
{
|
||||
std::string dir;
|
||||
@@ -317,6 +366,8 @@ void handle(int fd)
|
||||
respond(fd, 200, "application/json", out + "]");
|
||||
} else if (method == "GET" && path == "/api/files") {
|
||||
handle_files(fd, query);
|
||||
} else if (method == "GET" && path == "/api/download") {
|
||||
handle_download(fd, query);
|
||||
} else if (method == "PUT" && path == "/api/upload") {
|
||||
handle_upload(fd, query, length, body);
|
||||
} else if (method == "DELETE" && path == "/api/file") {
|
||||
@@ -328,38 +379,20 @@ void handle(int fd)
|
||||
finished = true; /* the library looks again */
|
||||
}
|
||||
respond(fd, ok ? 200 : 404, "text/plain", ok ? "ok" : "not found");
|
||||
} else if (method == "GET" && path == "/api/status") {
|
||||
WebStatus s;
|
||||
} else if (method == "GET" && path == "/api/opensubtitles") {
|
||||
WebOsubInfo s;
|
||||
bool waiting;
|
||||
{
|
||||
std::lock_guard<std::mutex> g(lock);
|
||||
s = status;
|
||||
s = osub_info;
|
||||
waiting = osub_sent; /* sent, not yet taken by the main thread */
|
||||
}
|
||||
std::string out = std::string("{\"playing\":") + (s.playing ? "true" : "false") +
|
||||
",\"paused\":" + (s.paused ? "true" : "false") +
|
||||
",\"title\":" + json_string(s.title) +
|
||||
",\"time\":" + std::to_string(s.time_ms) +
|
||||
",\"length\":" + std::to_string(s.length_ms) +
|
||||
",\"volume\":" + std::to_string(s.volume) + "}";
|
||||
std::string out = std::string("{\"key\":") + (s.has_key ? "true" : "false") +
|
||||
",\"checking\":" + (s.checking || waiting ? "true" : "false") +
|
||||
",\"failed\":" + (s.failed ? "true" : "false") +
|
||||
",\"user\":" + json_string(s.user) +
|
||||
",\"message\":" + json_string(s.message) + "}";
|
||||
respond(fd, 200, "application/json", out);
|
||||
} else if (method == "POST" && path == "/api/cmd") {
|
||||
std::string c = query_value(query, "c");
|
||||
int64_t v = atoll(query_value(query, "v").c_str());
|
||||
WebCommandKind k;
|
||||
bool known = true;
|
||||
if (c == "toggle") k = WEB_TOGGLE;
|
||||
else if (c == "jump") k = WEB_JUMP;
|
||||
else if (c == "seek") k = WEB_SEEK;
|
||||
else if (c == "next") k = WEB_NEXT;
|
||||
else if (c == "prev") k = WEB_PREV;
|
||||
else if (c == "vol") k = WEB_VOLUME;
|
||||
else if (c == "stop") k = WEB_STOP;
|
||||
else known = false;
|
||||
if (known) {
|
||||
std::lock_guard<std::mutex> g(lock);
|
||||
if (commands.size() < 32)
|
||||
commands.push_back({ k, v });
|
||||
}
|
||||
respond(fd, known ? 200 : 400, "text/plain", known ? "ok" : "unknown command");
|
||||
} else if (method == "POST" && path == "/api/opensubtitles") {
|
||||
/* a small form: the rest of its body, then key, user, pass */
|
||||
while ((int64_t)body.size() < length && length < 4096) {
|
||||
@@ -483,20 +516,10 @@ void web_set_places(const std::vector<WebPlace> &p)
|
||||
places = p;
|
||||
}
|
||||
|
||||
void web_set_status(const WebStatus &s)
|
||||
void web_set_osub_info(const WebOsubInfo &info)
|
||||
{
|
||||
std::lock_guard<std::mutex> g(lock);
|
||||
status = s;
|
||||
}
|
||||
|
||||
bool web_next_command(WebCommand *c)
|
||||
{
|
||||
std::lock_guard<std::mutex> g(lock);
|
||||
if (commands.empty())
|
||||
return false;
|
||||
*c = commands.front();
|
||||
commands.pop_front();
|
||||
return true;
|
||||
osub_info = info;
|
||||
}
|
||||
|
||||
WebUpload web_upload_state()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* VLC-PS5's web page for phones and computers on the same network: send
|
||||
* videos and music to the console (no FTP), and a remote for what's playing.
|
||||
* videos and music to the console (no FTP), and an OpenSubtitles key.
|
||||
* A small HTTP server on its own threads; the main thread hands it what it
|
||||
* may show (web_set_*) and takes the remote's commands (web_next_command).
|
||||
* may show (web_set_*) and takes what arrived (web_take_*).
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
@@ -24,24 +24,13 @@ struct WebPlace {
|
||||
};
|
||||
void web_set_places(const std::vector<WebPlace> &places);
|
||||
|
||||
/* What the remote shows. */
|
||||
struct WebStatus {
|
||||
bool playing; /* a file is open */
|
||||
bool paused;
|
||||
std::string title;
|
||||
int64_t time_ms, length_ms;
|
||||
int volume; /* 0..200 */
|
||||
bool loop;
|
||||
/* What the page shows about subtitle downloads: never the key or the
|
||||
* password, only whether a key is saved, the account and the last check. */
|
||||
struct WebOsubInfo {
|
||||
bool has_key, checking, failed; /* failed: the last check went wrong */
|
||||
std::string user, message;
|
||||
};
|
||||
void web_set_status(const WebStatus &s);
|
||||
|
||||
/* The remote's buttons, for the main thread. */
|
||||
enum WebCommandKind { WEB_TOGGLE, WEB_JUMP, WEB_SEEK, WEB_NEXT, WEB_PREV, WEB_VOLUME, WEB_STOP };
|
||||
struct WebCommand {
|
||||
WebCommandKind kind;
|
||||
int64_t value; /* JUMP: ms (+/-), SEEK: ms, VOLUME: +/- percent */
|
||||
};
|
||||
bool web_next_command(WebCommand *c);
|
||||
void web_set_osub_info(const WebOsubInfo &info);
|
||||
|
||||
/* An upload in progress (for the TV), and whether one finished since asked. */
|
||||
struct WebUpload {
|
||||
|
||||
+514
-191
@@ -1,7 +1,9 @@
|
||||
/*
|
||||
* The page VLC-PS5 serves to phones and computers (see web.cc): send files
|
||||
* and a remote. One file, no outside resources (the console may have no
|
||||
* internet), sized for a phone first.
|
||||
* The page VLC-PS5 serves to phones and computers (see web.cc): send files to
|
||||
* the console and set up subtitle downloads. One file, no outside resources
|
||||
* (the console may have no internet). It looks like the app's Modern look:
|
||||
* black, VLC orange, a glowing tab bar whose highlight glides, panels that
|
||||
* slide in from the side you went to, rows with the orange focus bar.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
@@ -12,248 +14,569 @@ static const char WEB_PAGE[] = R"VLCPAGE(<!doctype html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#0b0b0f">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<title>VLC for PS5</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b0b0f; --panel: #16161d; --panel2: #1e1e27; --line: #2a2a35;
|
||||
--fg: #f4f4f7; --dim: #9a9aa8; --faint: #62626f; --accent: #ff8800; --accent2: #ff5e14;
|
||||
--ok: #3fd6a4; --bad: #ff5d6c;
|
||||
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
--bg: #000; --bar: #0f0f13; --card: #121217; --raised: #1a1a21; --hi: #24242d; --line: #22222a;
|
||||
--fg: #f5f5f7; --dim: #a3a3b0; --faint: #6c6c79;
|
||||
--or: #ff8800; --or2: #ff5e14; --glow: rgba(255, 136, 0, .26); --soft: rgba(255, 136, 0, .1);
|
||||
--ok: #34d399; --bad: #f87171;
|
||||
--ease: cubic-bezier(.22, .8, .24, 1); --spring: cubic-bezier(.34, 1.4, .5, 1);
|
||||
--sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
|
||||
html, body { margin: 0; background: var(--bg); color: var(--fg); font-family: var(--sans); }
|
||||
body { padding: env(safe-area-inset-top, 0) 16px calc(24px + env(safe-area-inset-bottom, 0)); max-width: 640px; margin: 0 auto; }
|
||||
header { display: flex; align-items: center; gap: 12px; padding: 20px 0 16px; }
|
||||
header svg { width: 34px; height: 34px; flex: none; }
|
||||
header h1 { font-size: 20px; margin: 0; letter-spacing: -0.01em; }
|
||||
header p { margin: 2px 0 0; color: var(--dim); font-size: 13px; }
|
||||
.tabs { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; background: var(--panel); padding: 5px; border-radius: 12px; }
|
||||
.tabs button { font: inherit; font-weight: 600; font-size: 15px; color: var(--dim); background: none; border: 0; padding: 11px; border-radius: 9px; cursor: pointer; }
|
||||
.tabs button[aria-pressed="true"] { background: var(--accent); color: #140900; }
|
||||
section { margin-top: 18px; display: grid; gap: 14px; }
|
||||
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 16px; padding: 16px; display: grid; gap: 12px; }
|
||||
label.small { font-size: 12px; color: var(--faint); text-transform: uppercase; letter-spacing: .08em; }
|
||||
select { font: inherit; color: var(--fg); background: var(--panel2); border: 1px solid var(--line); border-radius: 10px; padding: 11px 12px; width: 100%; }
|
||||
.drop { display: grid; place-items: center; gap: 6px; text-align: center; padding: 30px 16px; border: 2px dashed var(--line); border-radius: 14px; cursor: pointer; }
|
||||
.drop.over { border-color: var(--accent); background: rgba(255,136,0,.06); }
|
||||
.drop b { font-size: 17px; }
|
||||
.drop span { color: var(--dim); font-size: 13px; }
|
||||
html { background: var(--bg); }
|
||||
body { margin: 0; min-height: 100vh; color: var(--fg); font: 15px/1.45 var(--sans); background: var(--bg);
|
||||
overflow-x: hidden; padding-bottom: calc(24px + env(safe-area-inset-bottom, 0)); }
|
||||
button, input { font: inherit; color: inherit; }
|
||||
button { cursor: pointer; }
|
||||
:focus-visible { outline: 2px solid var(--or); outline-offset: 2px; }
|
||||
|
||||
/* a slow orange glow behind everything, like the app's backdrop */
|
||||
.aura { position: fixed; inset: -20vh -20vw auto; height: 70vh; pointer-events: none; z-index: 0;
|
||||
background: radial-gradient(60% 60% at 50% 0%, rgba(255, 120, 0, .07), transparent 70%);
|
||||
animation: breathe 9s ease-in-out infinite; }
|
||||
@keyframes breathe { 50% { opacity: .55; transform: scale(1.06); } }
|
||||
|
||||
/* title row: the cone and VLC, as in the app */
|
||||
header { position: relative; z-index: 2; max-width: 980px; margin: 0 auto;
|
||||
padding: calc(20px + env(safe-area-inset-top, 0)) 16px 0; display: flex; align-items: center; gap: 12px; }
|
||||
.cone { width: 40px; height: 40px; flex: none; animation: drop .7s var(--spring) both; }
|
||||
@keyframes drop { from { transform: translateY(-16px) scale(.8); opacity: 0; } }
|
||||
header h1 { margin: 0; font-size: 24px; font-weight: 750; letter-spacing: -.02em; line-height: 1.1; }
|
||||
header h1 small { display: block; font-size: 12.5px; font-weight: 500; color: var(--faint); letter-spacing: 0; }
|
||||
.link { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: var(--dim);
|
||||
padding: 7px 12px; border-radius: 99px; background: var(--bar); border: 1px solid var(--line); }
|
||||
.link i { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); position: relative; }
|
||||
.link i::after { content: ""; position: absolute; inset: -4px; border-radius: 50%; border: 2px solid var(--ok);
|
||||
opacity: 0; animation: ping 2.4s ease-out infinite; }
|
||||
@keyframes ping { 0% { transform: scale(.4); opacity: .7; } 80%, 100% { transform: scale(1.4); opacity: 0; } }
|
||||
.link.off i, .link.off i::after { background: var(--bad); border-color: var(--bad); }
|
||||
|
||||
/* the tab bar: a dark box, the highlight glides under the chosen tab */
|
||||
nav { position: sticky; top: 0; z-index: 3; padding: 14px 16px 12px; max-width: 980px; margin: 0 auto;
|
||||
backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); }
|
||||
.tabs { position: relative; display: grid; grid-template-columns: repeat(3, 1fr); background: var(--bar);
|
||||
border: 1px solid var(--line); border-radius: 18px; padding: 6px; max-width: 620px; margin: 0 auto;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, .6); }
|
||||
.tabs .hl { position: absolute; top: 6px; bottom: 6px; left: 6px; width: 0; border-radius: 13px;
|
||||
background: linear-gradient(180deg, #ff9a1f, var(--or)); box-shadow: 0 0 14px var(--glow), 0 0 0 1px rgba(255, 170, 60, .3) inset;
|
||||
transition: left .45s var(--spring), width .45s var(--spring); }
|
||||
.tabs button { position: relative; z-index: 1; border: 0; background: none; color: var(--dim); font-weight: 650;
|
||||
font-size: 15px; padding: 11px 8px; border-radius: 13px; display: flex; align-items: center; justify-content: center;
|
||||
gap: 8px; transition: color .3s; white-space: nowrap; }
|
||||
.tabs button svg { width: 18px; height: 18px; flex: none; transition: transform .4s var(--spring); }
|
||||
.tabs button:hover { color: var(--fg); }
|
||||
.tabs button[aria-selected="true"] { color: #fff; }
|
||||
.tabs button[aria-selected="true"] svg { transform: scale(1.12); }
|
||||
.tabs .n { min-width: 20px; height: 20px; padding: 0 6px; border-radius: 10px; font-size: 11.5px; font-weight: 750;
|
||||
display: inline-grid; place-items: center; background: var(--hi); color: var(--dim); transition: background .3s, color .3s; }
|
||||
.tabs button[aria-selected="true"] .n { background: rgba(0, 0, 0, .25); color: #fff; }
|
||||
.tabs .n:empty { display: none; }
|
||||
@media (max-width: 480px) { .tabs button .t { display: none; } .tabs button { padding: 12px 6px; } }
|
||||
|
||||
/* panels slide in from the side you moved to */
|
||||
main { position: relative; z-index: 1; max-width: 980px; margin: 0 auto; padding: 8px 16px 0; }
|
||||
.panel { display: none; }
|
||||
.panel.on { display: grid; gap: 16px; animation: var(--in, in-r) .5s var(--ease) both; }
|
||||
@keyframes in-r { from { opacity: 0; transform: translateX(36px); } }
|
||||
@keyframes in-l { from { opacity: 0; transform: translateX(-36px); } }
|
||||
.panel > * { animation: rise .55s var(--ease) both; }
|
||||
.panel > :nth-child(2) { animation-delay: .06s; } .panel > :nth-child(3) { animation-delay: .12s; }
|
||||
.panel > :nth-child(4) { animation-delay: .18s; }
|
||||
@keyframes rise { from { opacity: 0; transform: translateY(14px); } }
|
||||
h2 { margin: 6px 4px 0; font-size: 26px; font-weight: 750; letter-spacing: -.02em; }
|
||||
.lead { margin: -8px 4px 0; color: var(--dim); }
|
||||
.card { background: var(--card); border: 1px solid var(--line); border-radius: 20px; padding: 18px; }
|
||||
@media (min-width: 760px) { .card { padding: 24px; } h2 { font-size: 30px; } }
|
||||
|
||||
/* where files go: chips, the chosen one glows */
|
||||
.places { display: flex; flex-wrap: wrap; gap: 8px; min-width: 0; }
|
||||
.places button { border: 1px solid var(--line); background: var(--raised); color: var(--dim); border-radius: 12px;
|
||||
padding: 9px 14px; font-size: 14px; font-weight: 600; display: inline-flex; align-items: center; gap: 8px;
|
||||
transition: all .3s var(--ease); max-width: 100%; }
|
||||
.places button svg { width: 16px; height: 16px; flex: none; }
|
||||
.places button span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.places button:hover { color: var(--fg); background: var(--hi); }
|
||||
.places button[aria-pressed="true"] { color: var(--fg); background: var(--soft); border-color: var(--or);
|
||||
box-shadow: 0 0 10px rgba(255, 136, 0, .12); }
|
||||
|
||||
/* the drop zone */
|
||||
.drop { position: relative; display: grid; place-items: center; text-align: center; gap: 4px; padding: 44px 16px;
|
||||
border-radius: 18px; cursor: pointer; background: var(--raised); overflow: hidden;
|
||||
transition: transform .35s var(--spring), background .3s; }
|
||||
.drop::before { content: ""; position: absolute; inset: 0; border-radius: inherit; padding: 2px;
|
||||
background: repeating-linear-gradient(90deg, var(--hi) 0 14px, transparent 14px 24px);
|
||||
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
||||
-webkit-mask-composite: xor; mask-composite: exclude; }
|
||||
.drop:hover { background: var(--hi); }
|
||||
.drop.over { transform: scale(1.02); background: var(--soft); }
|
||||
.drop.over::before { background: repeating-linear-gradient(90deg, var(--or) 0 14px, transparent 14px 24px);
|
||||
background-size: 24px 100%; animation: march .6s linear infinite; }
|
||||
@keyframes march { to { background-position: 24px 0; } }
|
||||
.drop .ic { width: 64px; height: 64px; border-radius: 20px; display: grid; place-items: center; margin-bottom: 10px;
|
||||
color: #fff; background: linear-gradient(180deg, #ff9a1f, var(--or2)); box-shadow: 0 6px 16px var(--glow);
|
||||
animation: float 3.2s ease-in-out infinite; }
|
||||
.drop .ic svg { width: 28px; height: 28px; }
|
||||
@keyframes float { 50% { transform: translateY(-6px); } }
|
||||
.drop b { font-size: 18px; font-weight: 700; }
|
||||
.drop span { color: var(--dim); font-size: 14px; }
|
||||
.drop input { display: none; }
|
||||
.big { font: inherit; font-weight: 700; font-size: 16px; color: #140900; background: var(--accent); border: 0; border-radius: 12px; padding: 13px 18px; cursor: pointer; }
|
||||
.list { display: grid; gap: 8px; }
|
||||
.item { display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 4px 10px; align-items: center; background: var(--panel2); border-radius: 12px; padding: 10px 12px; }
|
||||
.item .name { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.item .meta { grid-column: 1; font-size: 12px; color: var(--dim); font-variant-numeric: tabular-nums; }
|
||||
.item .bar { grid-column: 1 / -1; height: 5px; background: var(--line); border-radius: 3px; overflow: hidden; }
|
||||
.item .bar i { display: block; height: 100%; width: 0; background: linear-gradient(90deg, var(--accent2), var(--accent)); }
|
||||
.item.done .bar i { background: var(--ok); }
|
||||
.item.fail .bar i { background: var(--bad); }
|
||||
.del { font: inherit; font-size: 13px; color: var(--dim); background: none; border: 1px solid var(--line); border-radius: 8px; padding: 6px 10px; cursor: pointer; grid-row: 1 / span 2; grid-column: 2; }
|
||||
.empty { color: var(--faint); font-size: 14px; text-align: center; padding: 10px; }
|
||||
.now { display: grid; gap: 6px; }
|
||||
.now h2 { margin: 0; font-size: 18px; overflow-wrap: anywhere; }
|
||||
.now p { margin: 0; color: var(--dim); font-size: 13px; }
|
||||
input[type=range] { width: 100%; accent-color: var(--accent); }
|
||||
.times { display: flex; justify-content: space-between; color: var(--dim); font-size: 12px; font-variant-numeric: tabular-nums; }
|
||||
.pad { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; }
|
||||
.pad button, .row button { font: inherit; font-weight: 700; color: var(--fg); background: var(--panel2); border: 1px solid var(--line); border-radius: 14px; aspect-ratio: 1; cursor: pointer; display: grid; place-items: center; }
|
||||
.pad button svg { width: 26px; height: 26px; fill: currentColor; }
|
||||
.pad button.main { background: var(--fg); color: var(--bg); }
|
||||
.row { display: grid; grid-template-columns: 1fr auto 1fr auto; gap: 8px; align-items: center; }
|
||||
.row button { aspect-ratio: auto; padding: 12px; font-size: 18px; }
|
||||
.row span { text-align: center; font-variant-numeric: tabular-nums; color: var(--dim); }
|
||||
.field { font: inherit; color: var(--fg); background: var(--panel2); border: 1px solid var(--line); border-radius: 10px; padding: 11px 12px; width: 100%; }
|
||||
.note { margin: 0; color: var(--dim); font-size: 13px; }
|
||||
.note a { color: var(--accent); }
|
||||
button:active { transform: scale(.97); }
|
||||
button:focus-visible, select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
footer { margin-top: 22px; color: var(--faint); font-size: 12px; text-align: center; }
|
||||
|
||||
/* rows: the app's list, a lighter row with the orange bar on the left */
|
||||
.rows { display: grid; gap: 6px; }
|
||||
.rows:empty { display: none; }
|
||||
.row { position: relative; display: grid; grid-template-columns: 44px minmax(0, 1fr) auto; align-items: center;
|
||||
gap: 14px; padding: 10px 12px 10px 14px; border-radius: 14px;
|
||||
transition: background .25s, box-shadow .25s; }
|
||||
.row::before { content: ""; position: absolute; left: 0; top: 14px; bottom: 14px; width: 4px; border-radius: 2px;
|
||||
background: var(--or); transform: scaleY(0); transition: transform .3s var(--spring); }
|
||||
.row:hover { background: var(--raised); box-shadow: 0 0 0 1px rgba(255, 136, 0, .14); }
|
||||
.row:hover::before { transform: scaleY(1); }
|
||||
.row.new { animation: pop .45s var(--spring) both; }
|
||||
@keyframes pop { from { opacity: 0; transform: translateY(10px) scale(.97); } }
|
||||
.row.gone { animation: out .35s var(--ease) both; }
|
||||
@keyframes out { to { opacity: 0; transform: translateX(30px); } }
|
||||
.row .ic { width: 44px; height: 44px; border-radius: 12px; background: var(--hi); display: grid; place-items: center;
|
||||
color: var(--dim); transition: color .25s, background .25s; }
|
||||
.row .ic svg { width: 22px; height: 22px; }
|
||||
.row:hover .ic { color: var(--or); background: var(--soft); }
|
||||
.row .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.row .meta { font-size: 13px; color: var(--dim); font-variant-numeric: tabular-nums; display: flex; gap: 8px; align-items: center; }
|
||||
.tag { font-size: 10.5px; font-weight: 750; letter-spacing: .04em; padding: 2px 7px; border-radius: 6px;
|
||||
background: var(--hi); color: var(--dim); text-transform: uppercase; }
|
||||
.bar { height: 5px; margin-top: 8px; background: var(--hi); border-radius: 3px; overflow: hidden; }
|
||||
.bar i { position: relative; display: block; height: 100%; width: 0; border-radius: 3px; overflow: hidden;
|
||||
background: linear-gradient(90deg, var(--or2), var(--or)); box-shadow: 0 0 6px var(--glow); transition: width .3s linear; }
|
||||
.bar i::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .45), transparent);
|
||||
transform: translateX(-100%); animation: shine 1.3s ease-in-out infinite; }
|
||||
@keyframes shine { to { transform: translateX(100%); } }
|
||||
.row.done .bar i { background: var(--ok); box-shadow: 0 0 12px rgba(52, 211, 153, .5); }
|
||||
.row.done .bar i::after, .row.fail .bar i::after { display: none; }
|
||||
.row.fail .bar i { background: var(--bad); box-shadow: none; }
|
||||
.row.done .meta { color: var(--ok); } .row.fail .meta { color: var(--bad); }
|
||||
.act { border: 0; background: none; color: var(--faint); border-radius: 10px; padding: 8px 12px; font-size: 13.5px;
|
||||
font-weight: 600; transition: all .25s var(--ease); }
|
||||
.act:hover { color: var(--fg); background: var(--hi); }
|
||||
.act.sure { color: #fff; background: var(--bad); }
|
||||
.acts { display: flex; align-items: center; gap: 2px; }
|
||||
.act.dl { display: inline-flex; align-items: center; gap: 6px; text-decoration: none; }
|
||||
.act.dl svg { width: 16px; height: 16px; }
|
||||
.act.dl:hover { color: var(--or); background: var(--soft); }
|
||||
@media (max-width: 480px) { .act.dl span { display: none; } .act.dl { padding: 8px 10px; } }
|
||||
.round { width: 38px; height: 38px; padding: 0; display: grid; place-items: center; border-radius: 12px; flex: none; }
|
||||
.round svg { width: 18px; height: 18px; }
|
||||
.spin svg { animation: spin .7s var(--ease); }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.empty { display: grid; place-items: center; gap: 10px; padding: 36px 8px; color: var(--faint); text-align: center; }
|
||||
.empty svg { width: 54px; height: 54px; opacity: .5; }
|
||||
.headrow { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
|
||||
|
||||
/* subtitles */
|
||||
.status { display: flex; align-items: center; gap: 14px; }
|
||||
.status .ic { width: 52px; height: 52px; border-radius: 16px; flex: none; display: grid; place-items: center;
|
||||
background: var(--hi); color: var(--faint); transition: all .4s var(--ease); }
|
||||
.status .ic svg { width: 26px; height: 26px; }
|
||||
.status.ok .ic { background: rgba(52, 211, 153, .14); color: var(--ok); }
|
||||
.status.bad .ic { background: rgba(248, 113, 113, .14); color: var(--bad); }
|
||||
.status b { display: block; font-size: 16px; } .status span { color: var(--dim); font-size: 13.5px; }
|
||||
.form { display: grid; gap: 14px; margin-top: 20px; }
|
||||
.form .two { display: grid; gap: 14px; }
|
||||
@media (min-width: 760px) { .form .two { grid-template-columns: 1fr 1fr; } }
|
||||
.field { position: relative; }
|
||||
.field input { width: 100%; padding: 22px 14px 8px; background: var(--raised); border: 1px solid var(--line);
|
||||
border-radius: 14px; transition: border-color .25s, background .25s, box-shadow .25s; }
|
||||
.field label { position: absolute; left: 15px; top: 15px; color: var(--faint); pointer-events: none;
|
||||
transition: all .25s var(--ease); }
|
||||
.field input:focus { outline: none; border-color: var(--or); background: #0b0b0e; box-shadow: 0 0 0 4px var(--soft); }
|
||||
.field input:focus + label, .field input:not(:placeholder-shown) + label { top: 6px; font-size: 11.5px; color: var(--or); }
|
||||
.field label em { font-style: normal; color: var(--faint); margin-left: 6px; }
|
||||
.go { position: relative; overflow: hidden; border: 0; border-radius: 14px; padding: 15px 18px; font-weight: 750; font-size: 16px;
|
||||
color: #fff; background: linear-gradient(180deg, #ff9a1f, var(--or)); box-shadow: 0 4px 14px var(--glow);
|
||||
transition: transform .2s var(--spring), box-shadow .3s, opacity .3s; }
|
||||
.go:hover { transform: translateY(-2px); box-shadow: 0 6px 18px var(--glow); }
|
||||
.go:active { transform: scale(.98); }
|
||||
.go:disabled { opacity: .55; cursor: default; transform: none; }
|
||||
.go.busy::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .35), transparent);
|
||||
animation: shine 1s ease-in-out infinite; transform: translateX(-100%); }
|
||||
.help { color: var(--faint); font-size: 13.5px; margin: 0; }
|
||||
.steps { display: grid; gap: 12px; counter-reset: s; margin: 0; padding: 0; list-style: none; }
|
||||
.steps li { display: flex; gap: 12px; align-items: flex-start; color: var(--dim); font-size: 14px; }
|
||||
.steps li::before { counter-increment: s; content: counter(s); flex: none; width: 24px; height: 24px; border-radius: 8px;
|
||||
display: grid; place-items: center; font-size: 12px; font-weight: 750; background: var(--soft); color: var(--or); }
|
||||
.steps a { color: var(--or); font-weight: 600; text-decoration: none; }
|
||||
.steps a:hover { text-decoration: underline; }
|
||||
|
||||
/* toasts, as the app shows them */
|
||||
.toasts { position: fixed; z-index: 9; left: 50%; bottom: calc(20px + env(safe-area-inset-bottom, 0)); transform: translateX(-50%);
|
||||
display: grid; gap: 8px; justify-items: center; width: min(92vw, 420px); pointer-events: none; }
|
||||
.toast { display: flex; align-items: center; gap: 10px; padding: 12px 18px; border-radius: 16px; background: rgba(28, 28, 36, .92);
|
||||
backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid var(--line); font-weight: 600; font-size: 14.5px;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, .6); animation: tin .45s var(--spring) both; max-width: 100%; }
|
||||
.toast span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.toast i { width: 9px; height: 9px; border-radius: 50%; background: var(--or); box-shadow: 0 0 10px var(--glow); flex: none; }
|
||||
.toast.ok i { background: var(--ok); box-shadow: 0 0 10px rgba(52, 211, 153, .6); }
|
||||
.toast.bad i { background: var(--bad); box-shadow: 0 0 10px rgba(248, 113, 113, .6); }
|
||||
.toast.bye { animation: tout .35s var(--ease) both; }
|
||||
@keyframes tin { from { opacity: 0; transform: translateY(20px) scale(.9); } }
|
||||
@keyframes tout { to { opacity: 0; transform: translateY(10px) scale(.95); } }
|
||||
footer { position: relative; z-index: 1; text-align: center; color: var(--faint); font-size: 12.5px; padding: 28px 16px 0; }
|
||||
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="aura"></div>
|
||||
<header>
|
||||
<svg viewBox="0 0 64 64" aria-hidden="true"><path d="M32 4 9 56h46z" fill="#ff8800"/><path d="M24 22h16l2.6 6H21.4zM18.6 34h26.8l2.6 6H16z" fill="#fff"/><rect x="5" y="54" width="54" height="7" rx="2.5" fill="#ff5e14"/></svg>
|
||||
<div><h1>VLC for PS5</h1><p id="who">On your console</p></div>
|
||||
<svg class="cone" viewBox="0 0 64 64" aria-hidden="true"><path d="M32 4 9 56h46z" fill="#ff8800"/><path d="M24 22h16l2.6 6H21.4zM18.6 34h26.8l2.6 6H16z" fill="#fff"/><rect x="5" y="54" width="54" height="7" rx="2.5" fill="#ff5e14"/></svg>
|
||||
<h1>VLC<small>for PS5</small></h1>
|
||||
<span class="link" id="link"><i></i><span id="link-t">Connected</span></span>
|
||||
</header>
|
||||
|
||||
<div class="tabs" role="tablist">
|
||||
<button id="t-send" aria-pressed="true">Send files</button>
|
||||
<button id="t-remote" aria-pressed="false">Remote</button>
|
||||
</div>
|
||||
<nav><div class="tabs" role="tablist">
|
||||
<div class="hl" id="hl"></div>
|
||||
<button role="tab" data-tab="send"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V4M7 9l5-5 5 5M5 20h14"/></svg><span class="t">Send</span><span class="n" id="n-send"></span></button>
|
||||
<button role="tab" data-tab="files"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><span class="t">On the PS5</span><span class="n" id="n-files"></span></button>
|
||||
<button role="tab" data-tab="subs"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><path d="M7 13h4M13 13h4M7 16h7"/></svg><span class="t">Subtitles</span></button>
|
||||
</div></nav>
|
||||
|
||||
<section id="p-send">
|
||||
<div class="card">
|
||||
<label class="small" for="place">Save to</label>
|
||||
<select id="place"></select>
|
||||
<label class="drop" id="drop">
|
||||
<input id="pick" type="file" multiple accept="video/*,audio/*,.mkv,.avi,.ts,.m2ts,.webm,.flac,.opus,.srt,.ass,.ssa,.vtt,.m3u,.m3u8,.pls">
|
||||
<b>Choose videos or music</b>
|
||||
<span>or drop them here. Subtitles (.srt, .ass) too.</span>
|
||||
</label>
|
||||
<div class="list" id="queue"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<label class="small">In this folder</label>
|
||||
<div class="list" id="files"><div class="empty">Loading…</div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<label class="small" for="os-key">Subtitle downloads (OpenSubtitles)</label>
|
||||
<p class="note">Paste your own free API key from opensubtitles.com (Profile › API consumers). An account is optional and gives more downloads a day.</p>
|
||||
<input class="field" id="os-key" placeholder="API key" autocomplete="off" autocapitalize="off" spellcheck="false">
|
||||
<input class="field" id="os-user" placeholder="User name (optional)" autocomplete="username" autocapitalize="off">
|
||||
<input class="field" id="os-pass" type="password" placeholder="Password (optional)" autocomplete="current-password">
|
||||
<button class="big" id="os-save">Save on the PS5</button>
|
||||
<p class="note" id="os-msg"></p>
|
||||
</div>
|
||||
</section>
|
||||
<main>
|
||||
<section class="panel" id="p-send">
|
||||
<h2>Send files</h2>
|
||||
<p class="lead">Videos, music, photos, subtitles, anything. They show up in VLC right away.</p>
|
||||
<div class="card" style="display:grid;gap:16px">
|
||||
<div class="places" data-places></div>
|
||||
<label class="drop" id="drop">
|
||||
<input id="pick" type="file" multiple>
|
||||
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V4M7 9l5-5 5 5M5 20h14"/></svg></div>
|
||||
<b>Choose files</b>
|
||||
<span id="drop-t">or drop them anywhere on this page</span>
|
||||
</label>
|
||||
<div class="rows" id="queue"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="p-remote" hidden>
|
||||
<div class="card now">
|
||||
<h2 id="title">Nothing playing</h2>
|
||||
<p id="state">Start a video on the console</p>
|
||||
<input id="seek" type="range" min="0" max="1000" value="0" aria-label="Position">
|
||||
<div class="times"><span id="t-now">0:00</span><span id="t-len">0:00</span></div>
|
||||
</div>
|
||||
<div class="pad">
|
||||
<button data-c="prev" aria-label="Previous"><svg viewBox="0 0 24 24"><path d="M6 5h2v14H6zM9.5 12 19 5v14z"/></svg></button>
|
||||
<button data-c="jump" data-v="-10000" aria-label="Back 10 seconds"><svg viewBox="0 0 24 24"><path d="M11 6 3 12l8 6zM20 6l-8 6 8 6z"/></svg></button>
|
||||
<button data-c="toggle" class="main" aria-label="Play or pause"><svg viewBox="0 0 24 24" id="pp"><path d="M8 5v14l11-7z"/></svg></button>
|
||||
<button data-c="jump" data-v="10000" aria-label="Forward 10 seconds"><svg viewBox="0 0 24 24"><path d="m13 6 8 6-8 6zM4 6l8 6-8 6z"/></svg></button>
|
||||
<button data-c="next" aria-label="Next"><svg viewBox="0 0 24 24"><path d="M16 5h2v14h-2zM14.5 12 5 5v14z"/></svg></button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button data-c="vol" data-v="-10" aria-label="Volume down">−</button>
|
||||
<span id="vol">Volume 100%</span>
|
||||
<button data-c="vol" data-v="10" aria-label="Volume up">+</button>
|
||||
<button data-c="stop" aria-label="Stop">Stop</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel" id="p-files">
|
||||
<h2>On the PS5</h2>
|
||||
<p class="lead">What's in that folder: download a file to this device, or delete it from the PS5.</p>
|
||||
<div class="card">
|
||||
<div class="headrow"><div class="places" data-places></div>
|
||||
<button class="act round" id="refresh" aria-label="Refresh"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 11a8 8 0 1 0-2.3 5.7M20 4v7h-7"/></svg></button></div>
|
||||
<div class="rows" id="files"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>Works on the same Wi-Fi as the PS5. Nothing leaves your home network.</footer>
|
||||
<section class="panel" id="p-subs">
|
||||
<h2>Subtitle downloads</h2>
|
||||
<p class="lead">VLC finds subtitles on OpenSubtitles with your own free API key.</p>
|
||||
<div class="card">
|
||||
<div class="status" id="os-status"><div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="15" r="4"/><path d="m11 12 9-9M17 6l3 3M14 9l2 2"/></svg></div>
|
||||
<div><b id="os-title">No key yet</b><span id="os-sub">Paste one below to turn subtitle downloads on.</span></div></div>
|
||||
<form class="form" id="os-form" autocomplete="off">
|
||||
<div class="field"><input id="os-key" placeholder=" " autocapitalize="off" spellcheck="false"><label for="os-key">API key</label></div>
|
||||
<div class="two">
|
||||
<div class="field"><input id="os-user" placeholder=" " autocomplete="username" autocapitalize="off" spellcheck="false"><label for="os-user">User name<em>optional</em></label></div>
|
||||
<div class="field"><input id="os-pass" type="password" placeholder=" " autocomplete="current-password"><label for="os-pass">Password<em>optional</em></label></div>
|
||||
</div>
|
||||
<button class="go" id="os-save" type="submit">Save on the PS5</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card" style="display:grid;gap:14px">
|
||||
<ol class="steps">
|
||||
<li><span>Sign in on <a href="https://www.opensubtitles.com/consumers" target="_blank" rel="noopener">opensubtitles.com/consumers</a> and create an API consumer.</span></li>
|
||||
<li><span>Copy its API key and paste it above.</span></li>
|
||||
<li><span>An account is optional: it gives more downloads a day.</span></li>
|
||||
</ol>
|
||||
<p class="help">The key stays on your PS5. This page never shows it again.</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer>Works on the same network as the PS5. Nothing leaves your home.</footer>
|
||||
<div class="toasts" id="toasts"></div>
|
||||
|
||||
<script>
|
||||
const $ = s => document.querySelector(s);
|
||||
const fmtSize = b => b > 1e9 ? (b / 1e9).toFixed(2) + " GB" : (b / 1e6).toFixed(1) + " MB";
|
||||
const fmtTime = ms => { const s = Math.max(0, Math.floor(ms / 1000)), h = Math.floor(s / 3600),
|
||||
m = Math.floor(s / 60) % 60, x = String(s % 60).padStart(2, "0");
|
||||
return h ? h + ":" + String(m).padStart(2, "0") + ":" + x : m + ":" + x; };
|
||||
const esc = t => t.replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
||||
const $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
|
||||
const esc = t => String(t).replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
||||
const size = b => b >= 1e9 ? (b / 1e9).toFixed(2) + " GB" : b >= 1e6 ? (b / 1e6).toFixed(1) + " MB"
|
||||
: Math.max(1, Math.round(b / 1e3)) + " KB";
|
||||
const ext = n => { const i = n.lastIndexOf("."); return i > 0 ? n.slice(i + 1).toLowerCase() : ""; };
|
||||
const save = (k, v) => { try { localStorage.setItem(k, v); } catch (e) {} };
|
||||
const load = k => { try { return localStorage.getItem(k); } catch (e) { return null; } };
|
||||
|
||||
/* OpenSubtitles: the key and account go to the console, which keeps them. */
|
||||
$("#os-save").onclick = async () => {
|
||||
const body = "key=" + encodeURIComponent($("#os-key").value.trim()) +
|
||||
"&user=" + encodeURIComponent($("#os-user").value.trim()) +
|
||||
"&pass=" + encodeURIComponent($("#os-pass").value);
|
||||
const r = await fetch("/api/opensubtitles", { method: "POST", body,
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" } }).catch(() => null);
|
||||
$("#os-msg").textContent = r && r.ok ? "Saved. The PS5 checks it and shows the result."
|
||||
: "Couldn't reach the PS5";
|
||||
$("#os-pass").value = "";
|
||||
/* Icons by kind of file. */
|
||||
const P = 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
|
||||
const ICON = {
|
||||
video: `<svg viewBox="0 0 24 24" ${P}><rect x="3" y="5" width="18" height="14" rx="3"/><path d="m10 9 5 3-5 3z"/></svg>`,
|
||||
audio: `<svg viewBox="0 0 24 24" ${P}><path d="M9 18V5l11-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="17" cy="16" r="3"/></svg>`,
|
||||
image: `<svg viewBox="0 0 24 24" ${P}><rect x="3" y="4" width="18" height="16" rx="3"/><circle cx="9" cy="10" r="2"/><path d="m21 16-5-5-9 9"/></svg>`,
|
||||
sub: `<svg viewBox="0 0 24 24" ${P}><rect x="3" y="5" width="18" height="14" rx="3"/><path d="M7 13h4M13 13h4M7 16h7"/></svg>`,
|
||||
zip: `<svg viewBox="0 0 24 24" ${P}><path d="M6 3h9l4 4v14H6z"/><path d="M11 3v2M11 7v2M11 11v2M10 15h2v3h-2z"/></svg>`,
|
||||
file: `<svg viewBox="0 0 24 24" ${P}><path d="M6 3h9l4 4v14H6z"/><path d="M14 3v5h5"/></svg>`,
|
||||
folder: `<svg viewBox="0 0 24 24" ${P}><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>`,
|
||||
drive: `<svg viewBox="0 0 24 24" ${P}><rect x="3" y="7" width="18" height="10" rx="3"/><path d="M17 12h.01M7 12h6"/></svg>`,
|
||||
};
|
||||
const KIND = [["video", "mp4 mkv avi mov ts m2ts mts webm wmv flv mpg mpeg m4v 3gp ogv vob iso"],
|
||||
["audio", "mp3 flac m4a aac ogg opus wav wma alac ape aiff m3u m3u8 pls"],
|
||||
["image", "jpg jpeg png gif bmp webp tga"], ["sub", "srt ass ssa vtt sub idx sup"], ["zip", "zip rar 7z"]];
|
||||
const kind = n => { const x = ext(n); for (const [k, l] of KIND) if (l.split(" ").includes(x)) return k; return "file"; };
|
||||
|
||||
/* Tabs. */
|
||||
function show(tab) {
|
||||
$("#t-send").setAttribute("aria-pressed", tab === "send");
|
||||
$("#t-remote").setAttribute("aria-pressed", tab === "remote");
|
||||
$("#p-send").hidden = tab !== "send";
|
||||
$("#p-remote").hidden = tab !== "remote";
|
||||
try { localStorage.setItem("vlcps5-tab", tab); } catch (e) {}
|
||||
/* Toasts. */
|
||||
function toast(text, cls = "") {
|
||||
const t = document.createElement("div");
|
||||
t.className = "toast " + cls;
|
||||
t.innerHTML = `<i></i><span>${esc(text)}</span>`;
|
||||
$("#toasts").append(t);
|
||||
while ($("#toasts").children.length > 3) $("#toasts").firstChild.remove();
|
||||
setTimeout(() => { t.classList.add("bye"); setTimeout(() => t.remove(), 350); }, 3200);
|
||||
}
|
||||
$("#t-send").onclick = () => show("send");
|
||||
$("#t-remote").onclick = () => show("remote");
|
||||
try { if (localStorage.getItem("vlcps5-tab") === "remote") show("remote"); } catch (e) {}
|
||||
|
||||
/* Places and the files in the chosen one. */
|
||||
/* Connection. */
|
||||
let wasOnline = true;
|
||||
function online(ok) {
|
||||
$("#link").classList.toggle("off", !ok);
|
||||
$("#link-t").textContent = ok ? "Connected" : "Can't reach the PS5";
|
||||
if (ok && !wasOnline) toast("Connected to the PS5 again", "ok");
|
||||
wasOnline = ok;
|
||||
}
|
||||
async function api(url, opts) {
|
||||
try { const r = await fetch(url, opts); online(true); return r; }
|
||||
catch (e) { online(false); return null; }
|
||||
}
|
||||
|
||||
/* Tabs: the highlight glides to the tab, the panel slides in from that side. */
|
||||
const TABS = ["send", "files", "subs"];
|
||||
let tab = null;
|
||||
function moveHl() {
|
||||
const b = $(`.tabs [data-tab="${tab}"]`), hl = $("#hl");
|
||||
if (!b) return;
|
||||
hl.style.left = b.offsetLeft + "px";
|
||||
hl.style.width = b.offsetWidth + "px";
|
||||
}
|
||||
function show(name, first) {
|
||||
if (name === tab) return;
|
||||
const from = TABS.indexOf(tab), to = TABS.indexOf(name);
|
||||
tab = name;
|
||||
save("vlcps5-tab", name);
|
||||
$$(".tabs button").forEach(b => b.setAttribute("aria-selected", b.dataset.tab === name));
|
||||
$$(".panel").forEach(p => p.classList.remove("on"));
|
||||
const p = $("#p-" + name);
|
||||
p.style.setProperty("--in", first ? "rise" : to < from ? "in-l" : "in-r");
|
||||
void p.offsetWidth;
|
||||
p.classList.add("on");
|
||||
/* the first time the highlight just sits there; after, it glides */
|
||||
const hl = $("#hl");
|
||||
if (first) hl.style.transition = "none";
|
||||
moveHl();
|
||||
if (first) requestAnimationFrame(() => requestAnimationFrame(() => hl.style.transition = ""));
|
||||
if (name === "files") loadFiles();
|
||||
}
|
||||
$$(".tabs button").forEach(b => b.onclick = () => show(b.dataset.tab));
|
||||
addEventListener("resize", moveHl);
|
||||
/* swipe across the page (a phone) to change tab */
|
||||
let sx = null, sy = 0;
|
||||
addEventListener("touchstart", e => { if (e.target.closest("input")) return; sx = e.touches[0].clientX; sy = e.touches[0].clientY; }, { passive: true });
|
||||
addEventListener("touchend", e => {
|
||||
if (sx === null) return;
|
||||
const dx = e.changedTouches[0].clientX - sx, dy = e.changedTouches[0].clientY - sy;
|
||||
sx = null;
|
||||
if (Math.abs(dx) > 70 && Math.abs(dx) > Math.abs(dy) * 1.6) {
|
||||
const i = TABS.indexOf(tab) + (dx < 0 ? 1 : -1);
|
||||
if (i >= 0 && i < TABS.length) show(TABS[i]);
|
||||
}
|
||||
});
|
||||
|
||||
/* Where files go: the same choice on both tabs. */
|
||||
let places = [], place = +load("vlcps5-place") || 0;
|
||||
function drawPlaces() {
|
||||
$$("[data-places]").forEach(box => {
|
||||
box.innerHTML = places.map((p, i) => `<button type="button" data-i="${i}" aria-pressed="${i === place}">` +
|
||||
`${/usb|ext|drive/i.test(p) ? ICON.drive : ICON.folder}<span>${esc(p)}</span></button>`).join("");
|
||||
});
|
||||
}
|
||||
async function loadPlaces() {
|
||||
const places = await (await fetch("/api/places")).json();
|
||||
$("#place").innerHTML = places.map((p, i) => `<option value="${i}">${esc(p)}</option>`).join("");
|
||||
const r = await api("/api/places");
|
||||
if (!r) return setTimeout(loadPlaces, 3000);
|
||||
places = await r.json();
|
||||
if (place >= places.length) place = 0;
|
||||
drawPlaces();
|
||||
loadFiles();
|
||||
}
|
||||
async function loadFiles() {
|
||||
const list = await (await fetch("/api/files?dir=" + $("#place").value)).json();
|
||||
$("#files").innerHTML = list.length ? list.map(f =>
|
||||
`<div class="item"><div class="name">${esc(f.name)}</div><div class="meta">${fmtSize(f.size)}</div>
|
||||
<button class="del" data-name="${esc(f.name)}">Delete</button></div>`).join("")
|
||||
: `<div class="empty">No files here yet</div>`;
|
||||
document.addEventListener("click", e => {
|
||||
const b = e.target.closest("[data-places] button");
|
||||
if (!b) return;
|
||||
place = +b.dataset.i;
|
||||
save("vlcps5-place", place);
|
||||
drawPlaces();
|
||||
loadFiles(true);
|
||||
});
|
||||
|
||||
/* Files in that place. Only rows that weren't there before animate. */
|
||||
let shown = null;
|
||||
async function loadFiles(fresh) {
|
||||
const r = await api("/api/files?dir=" + place);
|
||||
if (!r) return;
|
||||
const list = await r.json();
|
||||
if (fresh) shown = null;
|
||||
$("#n-files").textContent = list.length || "";
|
||||
$("#files").innerHTML = list.length ? list.map(f => {
|
||||
const k = kind(f.name), isNew = !shown || !shown.has(f.name);
|
||||
return `<div class="row${isNew ? " new" : ""}"><div class="ic">${ICON[k]}</div>
|
||||
<div style="min-width:0"><div class="name" title="${esc(f.name)}">${esc(f.name)}</div>
|
||||
<div class="meta">${ext(f.name) ? `<span class="tag">${esc(ext(f.name).slice(0, 4))}</span>` : ""}${size(f.size)}</div></div>
|
||||
<div class="acts"><a class="act dl" href="/api/download?dir=${place}&name=${encodeURIComponent(f.name)}" download="${esc(f.name)}" data-name="${esc(f.name)}" title="Download"><svg viewBox="0 0 24 24" ${P}><path d="M12 4v11M7 10l5 5 5-5M5 20h14"/></svg><span>Download</span></a>
|
||||
<button class="act del" data-name="${esc(f.name)}">Delete</button></div></div>`;
|
||||
}).join("") : `<div class="empty">${ICON.folder}<span>No files here yet</span></div>`;
|
||||
shown = new Set(list.map(f => f.name));
|
||||
}
|
||||
$("#place").onchange = loadFiles;
|
||||
$("#refresh").onclick = () => {
|
||||
const b = $("#refresh");
|
||||
b.classList.remove("spin"); void b.offsetWidth; b.classList.add("spin");
|
||||
loadFiles(true);
|
||||
};
|
||||
/* Delete: a first tap turns it red, a second one deletes. */
|
||||
$("#files").onclick = async e => {
|
||||
const d = e.target.closest(".dl");
|
||||
if (d) { toast("Downloading " + d.dataset.name); return; } /* the browser saves it */
|
||||
const b = e.target.closest(".del");
|
||||
if (!b || !confirm("Delete " + b.dataset.name + " from the PS5?")) return;
|
||||
await fetch("/api/file?dir=" + $("#place").value + "&name=" + encodeURIComponent(b.dataset.name), { method: "DELETE" });
|
||||
loadFiles();
|
||||
if (!b) return;
|
||||
if (!b.classList.contains("sure")) {
|
||||
b.classList.add("sure"); b.textContent = "Delete?";
|
||||
setTimeout(() => { b.classList.remove("sure"); b.textContent = "Delete"; }, 2500);
|
||||
return;
|
||||
}
|
||||
const row = b.closest(".row");
|
||||
const r = await api("/api/file?dir=" + place + "&name=" + encodeURIComponent(b.dataset.name), { method: "DELETE" });
|
||||
if (r && r.ok) {
|
||||
row.classList.add("gone");
|
||||
toast("Deleted " + b.dataset.name);
|
||||
setTimeout(loadFiles, 350);
|
||||
} else {
|
||||
toast("Couldn't delete it", "bad");
|
||||
}
|
||||
};
|
||||
|
||||
/* Uploads, one after another, each with its bar. */
|
||||
/* Uploads, one after another; the Send tab shows how far the queue is. */
|
||||
const queue = [];
|
||||
let busy = false;
|
||||
let current = null;
|
||||
function badge() {
|
||||
const left = queue.length + (current ? 1 : 0);
|
||||
$("#n-send").textContent = !left ? "" : current && current.pct != null && !queue.length ? current.pct + "%" : left;
|
||||
}
|
||||
function addFiles(files) {
|
||||
if (!files.length) return;
|
||||
for (const f of files) {
|
||||
const el = document.createElement("div");
|
||||
el.className = "item";
|
||||
el.innerHTML = `<div class="name">${esc(f.name)}</div><div class="meta">Waiting · ${fmtSize(f.size)}</div><div class="bar"><i></i></div>`;
|
||||
$("#queue").prepend(el);
|
||||
queue.push({ f, el, dir: $("#place").value });
|
||||
el.className = "row new";
|
||||
el.innerHTML = `<div class="ic">${ICON[kind(f.name)]}</div>
|
||||
<div style="min-width:0"><div class="name">${esc(f.name)}</div><div class="meta">Waiting · ${size(f.size)}</div><div class="bar"><i></i></div></div>
|
||||
<button class="act round stop" aria-label="Cancel"><svg viewBox="0 0 24 24" ${P}><path d="M6 6l12 12M18 6 6 18"/></svg></button>`;
|
||||
const job = { f, el, dir: place };
|
||||
el.querySelector(".stop").onclick = () => {
|
||||
if (current === job) { job.cancelled = true; job.xhr.abort(); }
|
||||
else { queue.splice(queue.indexOf(job), 1); el.classList.add("gone"); setTimeout(() => el.remove(), 350); badge(); }
|
||||
};
|
||||
$("#queue").append(el);
|
||||
queue.push(job);
|
||||
}
|
||||
toast(files.length === 1 ? "Sending " + files[0].name : "Sending " + files.length + " files");
|
||||
if (tab !== "send") show("send");
|
||||
next();
|
||||
}
|
||||
function next() {
|
||||
if (busy || !queue.length) return;
|
||||
busy = true;
|
||||
const { f, el, dir } = queue.shift();
|
||||
const xhr = new XMLHttpRequest(), t0 = Date.now();
|
||||
xhr.open("PUT", "/api/upload?dir=" + dir + "&name=" + encodeURIComponent(f.name));
|
||||
badge();
|
||||
if (current || !queue.length) return;
|
||||
const job = current = queue.shift();
|
||||
const { f, el } = job, meta = el.querySelector(".meta"), bar = el.querySelector(".bar i");
|
||||
const xhr = job.xhr = new XMLHttpRequest(), t0 = Date.now();
|
||||
xhr.open("PUT", "/api/upload?dir=" + job.dir + "&name=" + encodeURIComponent(f.name));
|
||||
xhr.upload.onprogress = e => {
|
||||
const p = e.loaded / f.size, mbs = e.loaded / 1e6 / Math.max(0.5, (Date.now() - t0) / 1000);
|
||||
el.querySelector(".bar i").style.width = (p * 100).toFixed(1) + "%";
|
||||
el.querySelector(".meta").textContent = `${Math.round(p * 100)}% · ${fmtSize(e.loaded)} of ${fmtSize(f.size)} · ${mbs.toFixed(1)} MB/s`;
|
||||
const p = f.size ? e.loaded / f.size : 1, mbs = e.loaded / 1e6 / Math.max(.5, (Date.now() - t0) / 1000);
|
||||
job.pct = Math.round(p * 100);
|
||||
bar.style.width = (p * 100).toFixed(1) + "%";
|
||||
meta.textContent = `${job.pct}% · ${size(e.loaded)} of ${size(f.size)} · ${mbs.toFixed(1)} MB/s`;
|
||||
badge();
|
||||
};
|
||||
xhr.onloadend = () => {
|
||||
const ok = xhr.status === 200;
|
||||
el.classList.add(ok ? "done" : "fail");
|
||||
el.querySelector(".bar i").style.width = "100%";
|
||||
el.querySelector(".meta").textContent = ok ? "On the PS5 · " + fmtSize(f.size) : "Failed: " + (xhr.responseText || "connection lost");
|
||||
busy = false;
|
||||
loadFiles();
|
||||
bar.style.width = "100%";
|
||||
meta.textContent = ok ? "On the PS5 · " + size(f.size) : job.cancelled ? "Cancelled" : "Failed: " + (xhr.responseText || "connection lost");
|
||||
el.querySelector(".stop").remove();
|
||||
if (ok) {
|
||||
toast(f.name + " is on the PS5", "ok");
|
||||
setTimeout(() => { el.classList.add("gone"); setTimeout(() => el.remove(), 350); }, 4000);
|
||||
} else if (!job.cancelled) {
|
||||
toast("Couldn't send " + f.name, "bad");
|
||||
}
|
||||
current = null;
|
||||
if (job.dir === place) loadFiles();
|
||||
next();
|
||||
};
|
||||
xhr.send(f);
|
||||
}
|
||||
$("#pick").onchange = e => { addFiles(e.target.files); e.target.value = ""; };
|
||||
$("#pick").onchange = e => { addFiles([...e.target.files]); e.target.value = ""; };
|
||||
/* Drop anywhere: the zone lights up and its border marches. */
|
||||
const drop = $("#drop");
|
||||
drop.ondragover = e => { e.preventDefault(); drop.classList.add("over"); };
|
||||
drop.ondragleave = () => drop.classList.remove("over");
|
||||
drop.ondrop = e => { e.preventDefault(); drop.classList.remove("over"); addFiles(e.dataTransfer.files); };
|
||||
let depth = 0;
|
||||
const dropText = on => { drop.classList.toggle("over", on); $("#drop-t").textContent = on ? "Let go to send" : "or drop them anywhere on this page"; };
|
||||
addEventListener("dragenter", e => { e.preventDefault(); if (depth++ === 0) { if (tab !== "send") show("send"); dropText(true); } });
|
||||
addEventListener("dragleave", () => { if (--depth <= 0) { depth = 0; dropText(false); } });
|
||||
addEventListener("dragover", e => e.preventDefault());
|
||||
addEventListener("drop", e => { e.preventDefault(); depth = 0; dropText(false); addFiles([...e.dataTransfer.files]); });
|
||||
addEventListener("beforeunload", e => { if (current) { e.preventDefault(); e.returnValue = ""; } });
|
||||
|
||||
/* Remote. */
|
||||
let seeking = false;
|
||||
async function poll() {
|
||||
try {
|
||||
const s = await (await fetch("/api/status")).json();
|
||||
$("#title").textContent = s.playing ? s.title : "Nothing playing";
|
||||
$("#state").textContent = s.playing ? (s.paused ? "Paused" : "Playing") : "Start a video on the console";
|
||||
if (!seeking) $("#seek").value = s.length ? Math.round(s.time / s.length * 1000) : 0;
|
||||
$("#seek").dataset.len = s.length;
|
||||
$("#t-now").textContent = fmtTime(s.time);
|
||||
$("#t-len").textContent = fmtTime(s.length);
|
||||
$("#vol").textContent = "Volume " + s.volume + "%";
|
||||
$("#pp").innerHTML = s.playing && !s.paused ? '<path d="M7 5h4v14H7zm6 0h4v14h-4z"/>' : '<path d="M8 5v14l11-7z"/>';
|
||||
} catch (e) {
|
||||
$("#state").textContent = "Can't reach the PS5: is VLC still open?";
|
||||
/* OpenSubtitles: the PS5 keeps the key and checks it; the card shows how that went. */
|
||||
let waiting = false;
|
||||
async function osubStatus() {
|
||||
const r = await api("/api/opensubtitles");
|
||||
if (!r) return;
|
||||
const s = await r.json();
|
||||
const st = $("#os-status");
|
||||
st.classList.toggle("ok", s.key && !s.failed && !s.checking);
|
||||
st.classList.toggle("bad", s.key && s.failed && !s.checking);
|
||||
$("#os-title").textContent = !s.key ? "No key yet" : s.checking ? "Checking the key…" : s.failed ? "The key didn't work" : "Subtitle downloads are on";
|
||||
$("#os-sub").textContent = !s.key ? "Paste one below to turn subtitle downloads on."
|
||||
: s.checking ? "The PS5 is asking OpenSubtitles." : s.message ? s.message + (s.user ? " · " + s.user : "")
|
||||
: s.user ? "Signed in as " + s.user : "Using your API key";
|
||||
if (waiting && !s.checking && s.message) {
|
||||
waiting = false;
|
||||
$("#os-save").classList.remove("busy");
|
||||
toast(s.message, s.failed ? "bad" : "ok");
|
||||
}
|
||||
}
|
||||
document.querySelectorAll("[data-c]").forEach(b => b.onclick = () =>
|
||||
fetch("/api/cmd?c=" + b.dataset.c + "&v=" + (b.dataset.v || 0), { method: "POST" }).then(poll));
|
||||
$("#seek").oninput = () => { seeking = true; };
|
||||
$("#seek").onchange = () => {
|
||||
const ms = Math.round($("#seek").value / 1000 * (+$("#seek").dataset.len || 0));
|
||||
fetch("/api/cmd?c=seek&v=" + ms, { method: "POST" }).then(() => { seeking = false; poll(); });
|
||||
$("#os-form").onsubmit = async e => {
|
||||
e.preventDefault();
|
||||
const key = $("#os-key").value.trim();
|
||||
if (!key) { toast("Paste your API key first", "bad"); $("#os-key").focus(); return; }
|
||||
const btn = $("#os-save");
|
||||
btn.disabled = true; btn.classList.add("busy");
|
||||
const body = "key=" + encodeURIComponent(key) + "&user=" + encodeURIComponent($("#os-user").value.trim()) +
|
||||
"&pass=" + encodeURIComponent($("#os-pass").value);
|
||||
const r = await api("/api/opensubtitles", { method: "POST", body,
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" } });
|
||||
btn.disabled = false;
|
||||
$("#os-pass").value = "";
|
||||
if (r && r.ok) {
|
||||
$("#os-key").value = "";
|
||||
waiting = true;
|
||||
toast("Sent to the PS5, checking…");
|
||||
setTimeout(osubStatus, 600);
|
||||
setTimeout(() => { if (waiting) { waiting = false; btn.classList.remove("busy"); } }, 20000);
|
||||
} else {
|
||||
btn.classList.remove("busy");
|
||||
toast("Couldn't reach the PS5", "bad");
|
||||
}
|
||||
};
|
||||
setInterval(() => { if (!$("#p-remote").hidden) poll(); }, 1000);
|
||||
|
||||
/* first tab: the address's #send / #files / #subs, else the last one used */
|
||||
const want = location.hash.slice(1) || load("vlcps5-tab");
|
||||
show(TABS.includes(want) ? want : "send", true);
|
||||
addEventListener("load", moveHl);
|
||||
if (document.fonts) document.fonts.ready.then(moveHl);
|
||||
loadPlaces();
|
||||
poll();
|
||||
osubStatus();
|
||||
setInterval(osubStatus, 2500);
|
||||
setInterval(() => { if (!current && !document.hidden && tab === "files") loadFiles(); }, 15000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in new issue
Block a user