Add BorderBreaker aspect ratio controls

This commit is contained in:
Kelvin Acheampong 2025-11-22 22:00:44 +00:00
parent 81fa5c902d
commit 2e66df3ad7
9 changed files with 919 additions and 504 deletions

6
.gitignore vendored
View file

@ -1,3 +1,5 @@
/target
/stremio*.exe
engine.log
engine.err
/target
/stremio*.exe
/libmpv-2.dll

View file

@ -1,225 +1,227 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "Stremio"
#define MyAppExeName "stremio-shell-ng.exe"
#define MyAppExeLocation SourcePath + "..\target\x86_64-pc-windows-msvc\release\" + MyAppExeName
#define MyAppVersion() GetVersionComponents(MyAppExeLocation, Local[0], Local[1], Local[2], Local[3]), \
Str(Local[0]) + "." + Str(Local[1]) + "." + Str(Local[2])
#define MyAppPublisher "Smart Code OOD"
#define MyAppCopyright "Copyright © " + GetDateTimeString('yyyy', '', '') + " " + MyAppPublisher
#define MyAppURL "https://www.stremio.com/"
#define MyAppGoodbyeURL "https://www.strem.io/goodbye"
#define AssocTorrentExt ".torrent"
#define AssocTorrentKey StringChange(MyAppName, " ", "") + AssocTorrentExt
#define AssocTorrentDesc "Bittorrent seed file"
#define public Dependency_NoExampleSetup
#include "CodeDependencies.iss"
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{DD3870DA-AF3C-4C73-B010-72944AB610C6}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppCopyright={#MyAppCopyright}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
SetupMutex=StremioShellNgSetupsMutex,Global\StremioShellNgSetupsMutex
; Remove the following line to run in administrative install mode (install for all users.)
PrivilegesRequired=lowest
DisableReadyPage=yes
DisableDirPage=yes
DisableProgramGroupPage=yes
; DisableFinishedPage=yes
ChangesAssociations=yes
OutputBaseFilename={#MyAppName}Setup-v{#MyAppVersion}
OutputDir=..
Compression=lzma
SolidCompression=yes
WizardStyle=modern
LanguageDetectionMethod=uilanguage
ShowLanguageDialog=auto
CloseApplications=yes
WizardImageFile={#SourcePath}..\images\windows-installer.bmp
WizardSmallImageFile={#SourcePath}..\images\windows-installer-header.bmp
SetupIconFile={#SourcePath}..\images\stremio.ico
UninstallDisplayIcon={app}\{#MyAppExeName},0
#ifdef SIGN
SignTool=stremiosign
SignedUninstaller=yes
#endif
[Code]
function InitializeSetup: Boolean;
begin
Dependency_AddWebView2;
Result := True;
end;
function ShouldSkipPage(PageID: Integer): Boolean;
begin
{ Hide finish page if run app is selected }
if (PageID = wpFinished) and WizardIsTaskSelected('runapp') then
Result := True
else
Result := False;
end;
procedure CurPageChanged(CurPageID: Integer);
begin
case (CurPageID) of
wpSelectTasks: WizardForm.NextButton.Caption := SetupMessage(msgButtonInstall);
wpFinished: WizardForm.NextButton.Caption := SetupMessage(msgButtonFinish);
else
WizardForm.NextButton.Caption := SetupMessage(msgButtonNext);
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
var
ResultCode: Integer;
begin
if (CurStep = ssDone) and WizardIsTaskSelected('runapp') then
ExecAsOriginalUser(ExpandConstant('{app}\{#MyAppExeName}'), '', '', SW_SHOW, ewNoWait, ResultCode);
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
ErrorCode: Integer;
begin
case (CurUninstallStep) of
usPostUninstall: if MsgBox(ExpandConstant('{cm:RemoveDataFolder}'), mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then
DelTree(ExpandConstant('{app}'), True, True, True);
usDone: ShellExec('', ExpandConstant('{#MyAppGoodbyeURL}'), '', '', SW_SHOW, ewNoWait, ErrorCode);
end;
end;
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "armenian"; MessagesFile: "compiler:Languages\Armenian.isl"
Name: "brazilianportuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl"
Name: "bulgarian"; MessagesFile: "compiler:Languages\Bulgarian.isl"
Name: "catalan"; MessagesFile: "compiler:Languages\Catalan.isl"
Name: "corsican"; MessagesFile: "compiler:Languages\Corsican.isl"
Name: "czech"; MessagesFile: "compiler:Languages\Czech.isl"
Name: "danish"; MessagesFile: "compiler:Languages\Danish.isl"
Name: "dutch"; MessagesFile: "compiler:Languages\Dutch.isl"
Name: "finnish"; MessagesFile: "compiler:Languages\Finnish.isl"
Name: "french"; MessagesFile: "compiler:Languages\French.isl"
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
Name: "hebrew"; MessagesFile: "compiler:Languages\Hebrew.isl"
Name: "icelandic"; MessagesFile: "compiler:Languages\Icelandic.isl"
Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl"
Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl"
Name: "norwegian"; MessagesFile: "compiler:Languages\Norwegian.isl"
Name: "polish"; MessagesFile: "compiler:Languages\Polish.isl"
Name: "portuguese"; MessagesFile: "compiler:Languages\Portuguese.isl"
Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
Name: "slovak"; MessagesFile: "compiler:Languages\Slovak.isl"
Name: "slovenian"; MessagesFile: "compiler:Languages\Slovenian.isl"
Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl"
Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl"
Name: "ukrainian"; MessagesFile: "compiler:Languages\Ukrainian.isl"
[CustomMessages]
RemoveDataFolder=Remove all data and configuration?
english.RemoveDataFolder=Remove all data and configuration?
armenian.RemoveDataFolder=Հեռացնե՞լ բոլոր տվյալները և կոնֆիգուրացիան:
brazilianportuguese.RemoveDataFolder=Remover todos os dados e configuração?
bulgarian.RemoveDataFolder=Премахване на всички данни и конфигурация?
catalan.RemoveDataFolder=Vols suprimir totes les dades i la configuració?
corsican.RemoveDataFolder=Eliminate tutti i dati è a cunfigurazione?
czech.RemoveDataFolder=Odebrat všechna data a konfiguraci?
danish.RemoveDataFolder=Remove all data and configuration?
dutch.RemoveDataFolder=Remove all data and configuration?
finnish.RemoveDataFolder=Poistetaanko kaikki tiedot ja asetukset?
french.RemoveDataFolder=Supprimer toutes les données et la configuration ?
german.RemoveDataFolder=Alle Daten und Konfiguration entfernen?
hebrew.RemoveDataFolder=Remove all data and configuration?
icelandic.RemoveDataFolder=Fjarlægja öll gögn og stillingar?
italian.RemoveDataFolder=Rimuovere tutti i dati e la configurazione?
japanese.RemoveDataFolder=すべてのデータと構成を削除しますか?
norwegian.RemoveDataFolder=Vil du fjerne all data og konfigurasjon?
polish.RemoveDataFolder=Usunąć wszystkie dane i konfigurację?
portuguese.RemoveDataFolder=Remover todos os dados e configuração?
russian.RemoveDataFolder=Удалить все данные и конфигурацию?
slovak.RemoveDataFolder=Chcete odstrániť všetky údaje a konfiguráciu?
slovenian.RemoveDataFolder=Želite odstraniti vse podatke in konfiguracijo?
spanish.RemoveDataFolder=¿Eliminar todos los datos y la configuración?
turkish.RemoveDataFolder=Tüm veriler ve yapılandırma kaldırılsın mı?
ukrainian.RemoveDataFolder=Видалити всі дані та конфігурацію?
[Tasks]
Name: "runapp"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"
;Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "assoctorrent"; Description: "Associate {#MyAppName} with .torrent files"
[Files]
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
Source: "{#MyAppExeLocation}"; DestDir: "{app}"; Flags: ignoreversion signonce
Source: "{#SourcePath}..\libmpv-2.dll"; DestDir: "{app}"; Flags: ignoreversion signonce
Source: "{#SourcePath}..\bin\ffmpeg.exe"; DestDir: "{app}"; Flags: ignoreversion signonce
Source: "{#SourcePath}..\bin\ffprobe.exe"; DestDir: "{app}"; Flags: ignoreversion signonce
Source: "{#SourcePath}..\bin\stremio-runtime.exe"; DestDir: "{app}"; Flags: ignoreversion signonce
Source: "{#SourcePath}..\server.js"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\avcodec-58.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\avdevice-58.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\avfilter-7.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\avformat-58.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\avutil-56.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\postproc-55.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\swresample-3.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\swscale-5.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\vcruntime140.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\vcruntime140_1.dll"; DestDir: "{app}"; Flags: ignoreversion
[Registry]
; Associate .torrent files if assoctorrent task is selected
Root: HKA; Subkey: "Software\Classes\{#AssocTorrentExt}}\OpenWithProgids"; ValueType: string; ValueName: "{#AssocTorrentKey}"; ValueData: ""; Flags: uninsdeletevalue; Tasks: assoctorrent
Root: HKA; Subkey: "Software\Classes\{#AssocTorrentKey}"; ValueType: string; ValueName: ""; ValueData: "{#AssocTorrentDesc}"; Flags: uninsdeletekey; Tasks: assoctorrent
Root: HKA; Subkey: "Software\Classes\{#AssocTorrentKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey; Tasks: assoctorrent
Root: HKA; Subkey: "Software\Classes\{#AssocTorrentKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey; Tasks: assoctorrent
; stremio: protocol
Root: HKA; Subkey: "Software\Classes\stremio"; ValueType: string; ValueName: ""; ValueData: "URL:Stremio Protocol"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\stremio"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\stremio\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\stremio\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey
; magnet: protocol
Root: HKA; Subkey: "Software\Classes\magnet"; ValueType: string; ValueName: ""; ValueData: "URL:BitTorrent magnet"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\magnet"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\magnet\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\magnet\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".torrent"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".avi"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".asf"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".mkv"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".mp4"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".mov"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".ogg"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".ogv"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".wmv"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".srt"; ValueData: ""; Flags: uninsdeletekey
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
; This is used if the desktop shortcut is created by the [run] section.
; [UninstallDelete]
; Type: files; Name: "{autodesktop}\{#MyAppName}.lnk"
; We don't use the run section as the .torrent association is very hard to handle
; [Run]
; Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
; Filename: "cmd"; Parameters: "/c copy ""{autoprograms}\{#MyAppName}.lnk"" ""{autodesktop}"""; Description: "{cm:CreateDesktopIcon}"; Flags: postinstall skipifsilent shellexec runhidden waituntilterminated runascurrentuser
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "Stremio BorderBreaker"
#define MyAppExeName "stremio-shell-ng.exe"
#define MyAppExeLocation SourcePath + "..\target\x86_64-pc-windows-msvc\release\" + MyAppExeName
#define MyAppVersion() GetVersionComponents(MyAppExeLocation, Local[0], Local[1], Local[2], Local[3]), \
Str(Local[0]) + "." + Str(Local[1]) + "." + Str(Local[2])
#define MyAppPublisher "BorderBreaker"
#define MyAppCopyright "Copyright © " + GetDateTimeString('yyyy', '', '') + " " + MyAppPublisher
#define MyAppURL "https://stremio-borderbreaker.local/"
#define MyAppGoodbyeURL MyAppURL
#define AssocTorrentExt ".torrent"
#define AssocTorrentKey StringChange(MyAppName, " ", "") + AssocTorrentExt
#define AssocTorrentDesc "Bittorrent seed file"
#define public Dependency_NoExampleSetup
#include "CodeDependencies.iss"
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{5C3D0D2C-5B0D-4567-90A6-5D21C4EF8B52}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppCopyright={#MyAppCopyright}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
SetupMutex=StremioShellNgSetupsMutex,Global\StremioShellNgSetupsMutex
; Remove the following line to run in administrative install mode (install for all users.)
PrivilegesRequired=lowest
DisableReadyPage=yes
DisableDirPage=yes
DisableProgramGroupPage=yes
; DisableFinishedPage=yes
ChangesAssociations=yes
OutputBaseFilename={#MyAppName}Setup-v{#MyAppVersion}
OutputDir=..
Compression=lzma
SolidCompression=yes
WizardStyle=modern
LanguageDetectionMethod=uilanguage
ShowLanguageDialog=auto
CloseApplications=yes
WizardImageFile={#SourcePath}..\images\windows-installer.bmp
WizardSmallImageFile={#SourcePath}..\images\windows-installer-header.bmp
SetupIconFile={#SourcePath}..\images\stremio.ico
UninstallDisplayIcon={app}\{#MyAppExeName},0
#ifdef SIGN
SignTool=stremiosign
SignedUninstaller=yes
#endif
[Code]
function InitializeSetup: Boolean;
begin
Dependency_AddWebView2;
Result := True;
end;
function ShouldSkipPage(PageID: Integer): Boolean;
begin
{ Hide finish page if run app is selected }
if (PageID = wpFinished) and WizardIsTaskSelected('runapp') then
Result := True
else
Result := False;
end;
procedure CurPageChanged(CurPageID: Integer);
begin
case (CurPageID) of
wpSelectTasks: WizardForm.NextButton.Caption := SetupMessage(msgButtonInstall);
wpFinished: WizardForm.NextButton.Caption := SetupMessage(msgButtonFinish);
else
WizardForm.NextButton.Caption := SetupMessage(msgButtonNext);
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
var
ResultCode: Integer;
begin
if (CurStep = ssDone) and WizardIsTaskSelected('runapp') then
ExecAsOriginalUser(ExpandConstant('{app}\{#MyAppExeName}'), '', '', SW_SHOW, ewNoWait, ResultCode);
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
ErrorCode: Integer;
begin
case (CurUninstallStep) of
usPostUninstall: if MsgBox(ExpandConstant('{cm:RemoveDataFolder}'), mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then
DelTree(ExpandConstant('{app}'), True, True, True);
usDone: ShellExec('', ExpandConstant('{#MyAppGoodbyeURL}'), '', '', SW_SHOW, ewNoWait, ErrorCode);
end;
end;
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "armenian"; MessagesFile: "compiler:Languages\Armenian.isl"
Name: "brazilianportuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl"
Name: "bulgarian"; MessagesFile: "compiler:Languages\Bulgarian.isl"
Name: "catalan"; MessagesFile: "compiler:Languages\Catalan.isl"
Name: "corsican"; MessagesFile: "compiler:Languages\Corsican.isl"
Name: "czech"; MessagesFile: "compiler:Languages\Czech.isl"
Name: "danish"; MessagesFile: "compiler:Languages\Danish.isl"
Name: "dutch"; MessagesFile: "compiler:Languages\Dutch.isl"
Name: "finnish"; MessagesFile: "compiler:Languages\Finnish.isl"
Name: "french"; MessagesFile: "compiler:Languages\French.isl"
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
Name: "hebrew"; MessagesFile: "compiler:Languages\Hebrew.isl"
Name: "hungarian"; MessagesFile: "compiler:Languages\Hungarian.isl"
Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl"
Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl"
Name: "korean"; MessagesFile: "compiler:Languages\Korean.isl"
Name: "norwegian"; MessagesFile: "compiler:Languages\Norwegian.isl"
Name: "polish"; MessagesFile: "compiler:Languages\Polish.isl"
Name: "portuguese"; MessagesFile: "compiler:Languages\Portuguese.isl"
Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
Name: "slovak"; MessagesFile: "compiler:Languages\Slovak.isl"
Name: "slovenian"; MessagesFile: "compiler:Languages\Slovenian.isl"
Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl"
Name: "swedish"; MessagesFile: "compiler:Languages\Swedish.isl"
Name: "tamil"; MessagesFile: "compiler:Languages\Tamil.isl"
Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl"
Name: "ukrainian"; MessagesFile: "compiler:Languages\Ukrainian.isl"
[CustomMessages]
RemoveDataFolder=Remove all data and configuration?
english.RemoveDataFolder=Remove all data and configuration?
armenian.RemoveDataFolder=Հեռացնե՞լ բոլոր տվյալները և կոնֆիգուրացիան:
brazilianportuguese.RemoveDataFolder=Remover todos os dados e configuração?
bulgarian.RemoveDataFolder=Премахване на всички данни и конфигурация?
catalan.RemoveDataFolder=Vols suprimir totes les dades i la configuració?
corsican.RemoveDataFolder=Eliminate tutti i dati è a cunfigurazione?
czech.RemoveDataFolder=Odebrat všechna data a konfiguraci?
danish.RemoveDataFolder=Remove all data and configuration?
dutch.RemoveDataFolder=Remove all data and configuration?
finnish.RemoveDataFolder=Poistetaanko kaikki tiedot ja asetukset?
french.RemoveDataFolder=Supprimer toutes les données et la configuration ?
german.RemoveDataFolder=Alle Daten und Konfiguration entfernen?
hebrew.RemoveDataFolder=Remove all data and configuration?
italian.RemoveDataFolder=Rimuovere tutti i dati e la configurazione?
japanese.RemoveDataFolder=すべてのデータと構成を削除しますか?
norwegian.RemoveDataFolder=Vil du fjerne all data og konfigurasjon?
polish.RemoveDataFolder=Usunąć wszystkie dane i konfigurację?
portuguese.RemoveDataFolder=Remover todos os dados e configuração?
russian.RemoveDataFolder=Удалить все данные и конфигурацию?
slovak.RemoveDataFolder=Chcete odstrániť všetky údaje a konfiguráciu?
slovenian.RemoveDataFolder=Želite odstraniti vse podatke in konfiguracijo?
spanish.RemoveDataFolder=¿Eliminar todos los datos y la configuración?
turkish.RemoveDataFolder=Tüm veriler ve yapılandırma kaldırılsın mı?
ukrainian.RemoveDataFolder=Видалити всі дані та конфігурацію?
[Tasks]
Name: "runapp"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"
;Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "assoctorrent"; Description: "Associate {#MyAppName} with .torrent files"
[Files]
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
Source: "{#MyAppExeLocation}"; DestDir: "{app}"; Flags: ignoreversion signonce
Source: "{#SourcePath}..\libmpv-2.dll"; DestDir: "{app}"; Flags: ignoreversion signonce
Source: "{#SourcePath}..\bin\ffmpeg.exe"; DestDir: "{app}"; Flags: ignoreversion signonce
Source: "{#SourcePath}..\bin\ffprobe.exe"; DestDir: "{app}"; Flags: ignoreversion signonce
Source: "{#SourcePath}..\bin\stremio-runtime.exe"; DestDir: "{app}"; Flags: ignoreversion signonce
Source: "{#SourcePath}..\server.js"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\avcodec-58.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\avdevice-58.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\avfilter-7.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\avformat-58.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\avutil-56.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\postproc-55.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\swresample-3.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\swscale-5.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\vcruntime140.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\bin\vcruntime140_1.dll"; DestDir: "{app}"; Flags: ignoreversion
[Registry]
; Associate .torrent files if assoctorrent task is selected
Root: HKA; Subkey: "Software\Classes\{#AssocTorrentExt}}\OpenWithProgids"; ValueType: string; ValueName: "{#AssocTorrentKey}"; ValueData: ""; Flags: uninsdeletevalue; Tasks: assoctorrent
Root: HKA; Subkey: "Software\Classes\{#AssocTorrentKey}"; ValueType: string; ValueName: ""; ValueData: "{#AssocTorrentDesc}"; Flags: uninsdeletekey; Tasks: assoctorrent
Root: HKA; Subkey: "Software\Classes\{#AssocTorrentKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey; Tasks: assoctorrent
Root: HKA; Subkey: "Software\Classes\{#AssocTorrentKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey; Tasks: assoctorrent
; stremio: protocol
Root: HKA; Subkey: "Software\Classes\stremio"; ValueType: string; ValueName: ""; ValueData: "URL:Stremio Protocol"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\stremio"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\stremio\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\stremio\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey
; magnet: protocol
Root: HKA; Subkey: "Software\Classes\magnet"; ValueType: string; ValueName: ""; ValueData: "URL:BitTorrent magnet"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\magnet"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\magnet\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\magnet\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".torrent"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".avi"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".asf"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".mkv"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".mp4"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".mov"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".ogg"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".ogv"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".wmv"; ValueData: ""; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".srt"; ValueData: ""; Flags: uninsdeletekey
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
; This is used if the desktop shortcut is created by the [run] section.
; [UninstallDelete]
; Type: files; Name: "{autodesktop}\{#MyAppName}.lnk"
; We don't use the run section as the .torrent association is very hard to handle
; [Run]
; Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
; Filename: "cmd"; Parameters: "/c copy ""{autoprograms}\{#MyAppName}.lnk"" ""{autodesktop}"""; Description: "{cm:CreateDesktopIcon}"; Flags: postinstall skipifsilent shellexec runhidden waituntilterminated runascurrentuser

View file

@ -1,7 +1,7 @@
use native_windows_derive::NwgUi;
use native_windows_gui as nwg;
use rand::Rng;
use serde_json;
use serde_json::{self, json};
use std::{
cell::RefCell,
io::Read,
@ -16,6 +16,7 @@ use url::Url;
use winapi::um::{winbase::CREATE_BREAKAWAY_FROM_JOB, winuser::WS_EX_TOPMOST};
use crate::stremio_app::{
aspect_ratio::AspectController,
constants::{APP_NAME, UPDATE_ENDPOINT, UPDATE_INTERVAL, WINDOW_MIN_HEIGHT, WINDOW_MIN_WIDTH},
ipc::{RPCRequest, RPCResponse},
splash::SplashImage,
@ -84,6 +85,11 @@ pub struct MainWindow {
#[nwg_control]
#[nwg_events(OnNotice: [Self::on_focus_notice] )]
pub focus_notice: nwg::Notice,
#[nwg_control]
#[nwg_events(OnNotice: [Self::on_aspect_toggle_notice] )]
pub aspect_toggle_notice: nwg::Notice,
pub aspect_controller: RefCell<AspectController>,
pub aspect_player_tx: RefCell<Option<flume::Sender<String>>>,
}
impl MainWindow {
@ -135,13 +141,16 @@ impl MainWindow {
self.window.set_visible(!self.start_hidden);
self.tray.tray_show_hide.set_checked(!self.start_hidden);
let player_channel = self.player.channel.borrow();
let (player_tx, player_rx) = player_channel
.as_ref()
.expect("Cannont obtain communication channel for the Player");
let player_tx = player_tx.clone();
let player_rx = player_rx.clone();
{
*self.aspect_player_tx.borrow_mut() = Some(player_tx.clone());
self.aspect_controller.borrow().apply_current(&player_tx);
}
let web_channel = self.webview.channel.borrow();
let (web_tx, web_rx) = web_channel
@ -242,6 +251,7 @@ impl MainWindow {
let hide_splash_sender = self.hide_splash_notice.sender();
let focus_sender = self.focus_notice.sender();
let autoupdater_setup_mutex = self.autoupdater_setup_file.clone();
let aspect_toggle_sender_thread = self.aspect_toggle_notice.sender();
thread::spawn(move || loop {
if let Some(msg) = web_rx
.recv()
@ -296,6 +306,9 @@ impl MainWindow {
Some("win-focus") => {
focus_sender.notice();
}
Some("borderbreaker-cycle") => {
aspect_toggle_sender_thread.notice();
}
Some("autoupdater-notif-clicked") => {
// We've shown the "Update Available" notification
// and the user clicked on "Restart And Update"
@ -421,4 +434,32 @@ impl MainWindow {
self.tray.tray_show_hide.set_checked(self.window.visible());
self.transmit_window_visibility_change();
}
fn on_aspect_toggle_notice(&self) {
let player_tx_opt = self.aspect_player_tx.borrow().clone();
if let Some(player_tx) = player_tx_opt {
let mut controller = self.aspect_controller.borrow_mut();
controller.cycle();
controller.apply_current(&player_tx);
let label = format!(
"Aspect: {}",
controller
.current_mode()
.overlay_label(controller.display_ratio())
);
drop(controller);
self.broadcast_aspect_overlay(&label);
}
}
fn broadcast_aspect_overlay(&self, text: &str) {
if let Ok(web_channel) = self.webview.channel.try_borrow() {
if let Some((web_tx, _)) = web_channel.as_ref() {
let payload = json!({
"__borderbreakerOverlay": text,
})
.to_string();
let _ = web_tx.send(payload);
}
}
}
}

View file

@ -0,0 +1,289 @@
use std::{
env,
fs::{self, File},
io::{Read, Write},
path::{Path, PathBuf},
};
use flume::Sender;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use crate::stremio_app::{
stremio_player::{
BoolProp, FpProp, InMsg, InMsgArgs, InMsgFn, PropKey, PropVal, StrProp,
},
window_helper,
};
static CONFIG_DIR: Lazy<PathBuf> = Lazy::new(|| {
env::var("APPDATA")
.map(PathBuf::from)
.unwrap_or_else(|_| env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
.join("StremioBorderBreaker")
});
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum AspectMode {
AutoDetect,
FillCrop,
FitToScreen,
Ratio16x9,
Ratio4x3,
Ratio1x1,
Ratio21x9,
Ratio32x9,
Cinema,
}
impl AspectMode {
pub fn display_name(self) -> &'static str {
match self {
AspectMode::AutoDetect => "Auto",
AspectMode::FillCrop => "Fill (Crop)",
AspectMode::FitToScreen => "Fit to Screen",
AspectMode::Ratio16x9 => "16:9",
AspectMode::Ratio4x3 => "4:3",
AspectMode::Ratio1x1 => "1:1",
AspectMode::Ratio21x9 => "21:9 Ultrawide",
AspectMode::Ratio32x9 => "32:9 Super Ultrawide",
AspectMode::Cinema => "Cinema",
}
}
pub fn overlay_label(self, display_ratio: f32) -> String {
match self {
AspectMode::AutoDetect => {
format!("Auto ({:.2}:1)", display_ratio.max(0.01))
}
mode => mode.display_name().to_string(),
}
}
}
struct AspectSpec {
aspect_override: Option<f64>,
keep_aspect: bool,
panscan: f64,
video_unscaled: Option<&'static str>,
}
impl AspectMode {
fn spec(self, display_ratio: f32) -> AspectSpec {
match self {
AspectMode::AutoDetect => AspectSpec {
aspect_override: Some(display_ratio.max(0.1) as f64),
keep_aspect: true,
panscan: 0.0,
video_unscaled: Some("no"),
},
AspectMode::FillCrop => AspectSpec {
aspect_override: None,
keep_aspect: true,
panscan: 1.0,
video_unscaled: Some("no"),
},
AspectMode::FitToScreen => AspectSpec {
aspect_override: None,
keep_aspect: true,
panscan: 0.0,
video_unscaled: Some("no"),
},
AspectMode::Ratio16x9 => AspectSpec::ratio(16.0 / 9.0),
AspectMode::Ratio4x3 => AspectSpec::ratio(4.0 / 3.0),
AspectMode::Ratio1x1 => AspectSpec::ratio(1.0),
AspectMode::Ratio21x9 => AspectSpec::ratio(21.0 / 9.0),
AspectMode::Ratio32x9 => AspectSpec::ratio(32.0 / 9.0),
AspectMode::Cinema => AspectSpec::ratio(2.39),
}
}
}
impl AspectSpec {
fn ratio(value: f64) -> Self {
AspectSpec {
aspect_override: Some(value),
keep_aspect: true,
panscan: 0.0,
video_unscaled: Some("no"),
}
}
}
#[derive(Serialize, Deserialize)]
struct AspectConfig {
mode: AspectMode,
}
pub struct AspectController {
config_path: PathBuf,
order: Vec<AspectMode>,
current_index: usize,
display_ratio: f32,
}
impl AspectController {
pub fn new() -> Self {
Self::with_paths(CONFIG_DIR.join("aspect.json"), window_helper::primary_monitor_ratio())
}
fn with_paths(config_path: PathBuf, display_ratio: f32) -> Self {
let order = vec![
AspectMode::AutoDetect,
AspectMode::FillCrop,
AspectMode::FitToScreen,
AspectMode::Ratio16x9,
AspectMode::Ratio4x3,
AspectMode::Ratio1x1,
AspectMode::Ratio21x9,
AspectMode::Ratio32x9,
AspectMode::Cinema,
];
let saved_mode = Self::read_config(&config_path).map(|c| c.mode);
let current_index = saved_mode
.and_then(|mode| order.iter().position(|m| m == &mode))
.unwrap_or(0);
AspectController {
config_path,
order,
current_index,
display_ratio,
}
}
pub fn current_mode(&self) -> AspectMode {
self.order[self.current_index]
}
pub fn cycle(&mut self) -> AspectMode {
self.current_index = (self.current_index + 1) % self.order.len();
self.persist();
self.current_mode()
}
pub fn apply_current(&self, player_tx: &Sender<String>) {
self.apply_mode(player_tx, self.current_mode());
}
pub fn apply_mode(&self, player_tx: &Sender<String>, mode: AspectMode) {
let spec = mode.spec(self.display_ratio);
if let Some(ratio) = spec.aspect_override {
send_fp_prop(player_tx, FpProp::VideoAspectOverride, ratio);
} else {
send_fp_prop(player_tx, FpProp::VideoAspectOverride, 0.0);
}
send_bool_prop(player_tx, BoolProp::Keepaspect, spec.keep_aspect);
send_fp_prop(player_tx, FpProp::Panscan, spec.panscan);
if let Some(value) = spec.video_unscaled {
send_str_prop(player_tx, StrProp::VideoUnscaled, value);
}
}
pub fn display_ratio(&self) -> f32 {
self.display_ratio
}
fn persist(&self) {
if let Some(parent) = self.config_path.parent() {
let _ = fs::create_dir_all(parent);
}
let config = AspectConfig {
mode: self.current_mode(),
};
if let Ok(data) = serde_json::to_vec(&config) {
if let Ok(mut file) = File::create(&self.config_path) {
let _ = file.write_all(&data);
}
}
}
fn read_config(path: &Path) -> Option<AspectConfig> {
let mut file = File::open(path).ok()?;
let mut buf = Vec::new();
file.read_to_end(&mut buf).ok()?;
serde_json::from_slice(&buf).ok()
}
}
impl Default for AspectController {
fn default() -> Self {
Self::new()
}
}
fn send_fp_prop(player_tx: &Sender<String>, prop: FpProp, value: f64) {
let msg = InMsg(
InMsgFn::MpvSetProp,
InMsgArgs::StProp(PropKey::Fp(prop), PropVal::Num(value)),
);
if let Ok(serialized) = serde_json::to_string(&msg) {
let _ = player_tx.send(serialized);
}
}
fn send_bool_prop(player_tx: &Sender<String>, prop: BoolProp, value: bool) {
let msg = InMsg(
InMsgFn::MpvSetProp,
InMsgArgs::StProp(PropKey::Bool(prop), PropVal::Bool(value)),
);
if let Ok(serialized) = serde_json::to_string(&msg) {
let _ = player_tx.send(serialized);
}
}
fn send_str_prop(player_tx: &Sender<String>, prop: StrProp, value: &str) {
let msg = InMsg(
InMsgFn::MpvSetProp,
InMsgArgs::StProp(PropKey::Str(prop), PropVal::Str(value.to_string())),
);
if let Ok(serialized) = serde_json::to_string(&msg) {
let _ = player_tx.send(serialized);
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{distributions::Alphanumeric, Rng};
use std::fs;
fn temp_config_path() -> PathBuf {
let mut name: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(16)
.map(char::from)
.collect();
name.push_str(".json");
env::temp_dir().join(name)
}
#[test]
fn cycles_modes_and_persists() {
let path = temp_config_path();
let mut controller = AspectController::with_paths(path.clone(), 2.33);
assert_eq!(controller.current_mode(), AspectMode::AutoDetect);
controller.cycle();
assert_eq!(controller.current_mode(), AspectMode::FillCrop);
controller.cycle();
assert_eq!(controller.current_mode(), AspectMode::FitToScreen);
// ensure persisted
let loaded = AspectController::with_paths(path.clone(), 2.33);
assert_eq!(loaded.current_mode(), AspectMode::FitToScreen);
let _ = fs::remove_file(path);
}
#[test]
fn overlay_labels_match_modes() {
let ratio = 21.0 / 9.0;
assert_eq!(
AspectMode::AutoDetect.overlay_label(ratio),
format!("Auto ({:.2}:1)", ratio)
);
assert_eq!(
AspectMode::Ratio21x9.overlay_label(ratio),
"21:9 Ultrawide"
);
assert_eq!(AspectMode::Cinema.overlay_label(ratio), "Cinema");
}
}

View file

@ -1,5 +1,6 @@
pub mod app;
pub use app::MainWindow;
pub mod aspect_ratio;
pub mod ipc;
pub mod stremio_player;
pub mod stremio_server;

View file

@ -1,262 +1,267 @@
use core::convert::TryFrom;
use libmpv2::{events::PropertyData, mpv_end_file_reason, EndFileReason};
use parse_display::{Display, FromStr};
use serde::{Deserialize, Serialize};
use std::fmt;
// Responses
const JSON_RESPONSES: [&str; 3] = ["track-list", "video-params", "metadata"];
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
pub struct PlayerProprChange {
name: String,
data: serde_json::Value,
}
impl PlayerProprChange {
fn value_from_format(data: PropertyData, as_json: bool) -> serde_json::Value {
match data {
PropertyData::Flag(d) => serde_json::Value::Bool(d),
PropertyData::Int64(d) => serde_json::Value::Number(
serde_json::Number::from_f64(d as f64).expect("MPV returned invalid number"),
),
PropertyData::Double(d) => serde_json::Value::Number(
serde_json::Number::from_f64(d).expect("MPV returned invalid number"),
),
PropertyData::OsdStr(s) => serde_json::Value::String(s.to_string()),
PropertyData::Str(s) => {
if as_json {
serde_json::from_str(s).expect("MPV returned invalid JSON data")
} else {
serde_json::Value::String(s.to_string())
}
}
PropertyData::Node(_) => unimplemented!("`PropertyData::Node` is not supported"),
}
}
pub fn from_name_value(name: String, value: PropertyData) -> Self {
let is_json = JSON_RESPONSES.contains(&name.as_str());
Self {
name,
data: Self::value_from_format(value, is_json),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
pub struct PlayerEnded {
reason: String,
}
impl PlayerEnded {
fn string_from_end_reason(data: EndFileReason) -> String {
match data {
mpv_end_file_reason::Error => "error".to_string(),
mpv_end_file_reason::Quit => "quit".to_string(),
_ => "other".to_string(),
}
}
pub fn from_end_reason(data: EndFileReason) -> Self {
Self {
reason: Self::string_from_end_reason(data),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PlayerError {
pub error: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum PlayerEvent {
PropChange(PlayerProprChange),
End(PlayerEnded),
Error(PlayerError),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PlayerResponse<'a>(pub &'a str, pub PlayerEvent);
impl PlayerResponse<'_> {
pub fn to_value(&self) -> Option<serde_json::Value> {
serde_json::to_value(self).ok()
}
}
// Player incoming messages from the web UI
/*
Message general case - ["function-name", ["arguments", ...]]
The function could be either mpv-observe-prop, mpv-set-prop or mpv-command.
["mpv-observe-prop", "prop-name"]
["mpv-set-prop", ["prop-name", prop-val]]
["mpv-command", ["command-name"<, "arguments">]]
All the function and property names are in kebab-case.
MPV requires type for any prop-name when observing or setting it's value.
The type for setting is not always the same as the type for observing the prop.
"mpv-observe-prop" function is the only one that accepts single string
instead of array of arguments
"mpv-command" function always takes an array even if the command doesn't
have any arguments. For example this are the commands we support:
["mpv-command", ["loadfile", "file name"]]
["mpv-command", ["stop"]]
*/
macro_rules! stringable {
($t:ident) => {
impl From<$t> for String {
fn from(s: $t) -> Self {
s.to_string()
}
}
impl TryFrom<String> for $t {
type Error = parse_display::ParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
};
}
#[allow(clippy::enum_variant_names)]
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")]
pub enum InMsgFn {
MpvSetProp,
MpvCommand,
MpvObserveProp,
}
stringable!(InMsgFn);
// Bool
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")]
pub enum BoolProp {
Pause,
PausedForCache,
Seeking,
EofReached,
}
stringable!(BoolProp);
// Int
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")]
pub enum IntProp {
Aid,
Vid,
Sid,
}
stringable!(IntProp);
// Fp
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")]
pub enum FpProp {
TimePos,
Mute,
Volume,
Duration,
SubDelay,
SubScale,
CacheBufferingState,
SubPos,
Speed,
}
stringable!(FpProp);
// Str
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")]
pub enum StrProp {
FfmpegVersion,
Hwdec,
InputDefaltBindings,
InputVoKeyboard,
Metadata,
MpvVersion,
Osc,
Path,
SubAssOverride,
SubBackColor,
SubBorderColor,
SubColor,
TrackList,
VideoParams,
Vo,
}
stringable!(StrProp);
// Any
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(untagged)]
pub enum PropKey {
Bool(BoolProp),
Int(IntProp),
Fp(FpProp),
Str(StrProp),
}
impl fmt::Display for PropKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Bool(v) => write!(f, "{v}"),
Self::Int(v) => write!(f, "{v}"),
Self::Fp(v) => write!(f, "{v}"),
Self::Str(v) => write!(f, "{v}"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)]
pub enum PropVal {
Bool(bool),
Str(String),
Num(f64),
}
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")]
#[serde(untagged)]
pub enum MpvCmd {
Loadfile,
Stop,
}
stringable!(MpvCmd);
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(untagged)]
pub enum CmdVal {
Single((MpvCmd,)),
Double(MpvCmd, String),
Tripple(MpvCmd, String, String),
Quadruple(MpvCmd, String, String, String),
Quintuple(MpvCmd, String, String, String, String),
}
impl From<CmdVal> for Vec<String> {
fn from(cmd: CmdVal) -> Vec<String> {
match cmd {
CmdVal::Single(cmd) => vec![cmd.0.to_string()],
CmdVal::Double(cmd, arg) => vec![cmd.to_string(), arg],
CmdVal::Tripple(cmd, arg1, arg2) => vec![cmd.to_string(), arg1, arg2],
CmdVal::Quadruple(cmd, arg1, arg2, arg3) => vec![cmd.to_string(), arg1, arg2, arg3],
CmdVal::Quintuple(cmd, arg1, arg2, arg3, arg4) => {
vec![cmd.to_string(), arg1, arg2, arg3, arg4]
}
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)]
pub enum InMsgArgs {
StProp(PropKey, PropVal),
Cmd(CmdVal),
ObProp(PropKey),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct InMsg(pub InMsgFn, pub InMsgArgs);
use core::convert::TryFrom;
use libmpv2::{events::PropertyData, mpv_end_file_reason, EndFileReason};
use parse_display::{Display, FromStr};
use serde::{Deserialize, Serialize};
use std::fmt;
// Responses
const JSON_RESPONSES: [&str; 3] = ["track-list", "video-params", "metadata"];
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
pub struct PlayerProprChange {
name: String,
data: serde_json::Value,
}
impl PlayerProprChange {
fn value_from_format(data: PropertyData, as_json: bool) -> serde_json::Value {
match data {
PropertyData::Flag(d) => serde_json::Value::Bool(d),
PropertyData::Int64(d) => serde_json::Value::Number(
serde_json::Number::from_f64(d as f64).expect("MPV returned invalid number"),
),
PropertyData::Double(d) => serde_json::Value::Number(
serde_json::Number::from_f64(d).expect("MPV returned invalid number"),
),
PropertyData::OsdStr(s) => serde_json::Value::String(s.to_string()),
PropertyData::Str(s) => {
if as_json {
serde_json::from_str(s).expect("MPV returned invalid JSON data")
} else {
serde_json::Value::String(s.to_string())
}
}
PropertyData::Node(_) => unimplemented!("`PropertyData::Node` is not supported"),
}
}
pub fn from_name_value(name: String, value: PropertyData) -> Self {
let is_json = JSON_RESPONSES.contains(&name.as_str());
Self {
name,
data: Self::value_from_format(value, is_json),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
pub struct PlayerEnded {
reason: String,
}
impl PlayerEnded {
fn string_from_end_reason(data: EndFileReason) -> String {
match data {
mpv_end_file_reason::Error => "error".to_string(),
mpv_end_file_reason::Quit => "quit".to_string(),
_ => "other".to_string(),
}
}
pub fn from_end_reason(data: EndFileReason) -> Self {
Self {
reason: Self::string_from_end_reason(data),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PlayerError {
pub error: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum PlayerEvent {
PropChange(PlayerProprChange),
End(PlayerEnded),
Error(PlayerError),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PlayerResponse<'a>(pub &'a str, pub PlayerEvent);
impl PlayerResponse<'_> {
pub fn to_value(&self) -> Option<serde_json::Value> {
serde_json::to_value(self).ok()
}
}
// Player incoming messages from the web UI
/*
Message general case - ["function-name", ["arguments", ...]]
The function could be either mpv-observe-prop, mpv-set-prop or mpv-command.
["mpv-observe-prop", "prop-name"]
["mpv-set-prop", ["prop-name", prop-val]]
["mpv-command", ["command-name"<, "arguments">]]
All the function and property names are in kebab-case.
MPV requires type for any prop-name when observing or setting it's value.
The type for setting is not always the same as the type for observing the prop.
"mpv-observe-prop" function is the only one that accepts single string
instead of array of arguments
"mpv-command" function always takes an array even if the command doesn't
have any arguments. For example this are the commands we support:
["mpv-command", ["loadfile", "file name"]]
["mpv-command", ["stop"]]
*/
macro_rules! stringable {
($t:ident) => {
impl From<$t> for String {
fn from(s: $t) -> Self {
s.to_string()
}
}
impl TryFrom<String> for $t {
type Error = parse_display::ParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
};
}
#[allow(clippy::enum_variant_names)]
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")]
pub enum InMsgFn {
MpvSetProp,
MpvCommand,
MpvObserveProp,
}
stringable!(InMsgFn);
// Bool
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")]
pub enum BoolProp {
Pause,
PausedForCache,
Seeking,
EofReached,
Keepaspect,
}
stringable!(BoolProp);
// Int
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")]
pub enum IntProp {
Aid,
Vid,
Sid,
}
stringable!(IntProp);
// Fp
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")]
pub enum FpProp {
TimePos,
Mute,
Volume,
Duration,
SubDelay,
SubScale,
CacheBufferingState,
SubPos,
Speed,
VideoAspectOverride,
VideoZoom,
Panscan,
}
stringable!(FpProp);
// Str
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")]
pub enum StrProp {
FfmpegVersion,
Hwdec,
InputDefaltBindings,
InputVoKeyboard,
Metadata,
MpvVersion,
Osc,
Path,
SubAssOverride,
SubBackColor,
SubBorderColor,
SubColor,
TrackList,
VideoParams,
Vo,
VideoUnscaled,
}
stringable!(StrProp);
// Any
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(untagged)]
pub enum PropKey {
Bool(BoolProp),
Int(IntProp),
Fp(FpProp),
Str(StrProp),
}
impl fmt::Display for PropKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Bool(v) => write!(f, "{v}"),
Self::Int(v) => write!(f, "{v}"),
Self::Fp(v) => write!(f, "{v}"),
Self::Str(v) => write!(f, "{v}"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)]
pub enum PropVal {
Bool(bool),
Str(String),
Num(f64),
}
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")]
#[serde(untagged)]
pub enum MpvCmd {
Loadfile,
Stop,
}
stringable!(MpvCmd);
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(untagged)]
pub enum CmdVal {
Single((MpvCmd,)),
Double(MpvCmd, String),
Tripple(MpvCmd, String, String),
Quadruple(MpvCmd, String, String, String),
Quintuple(MpvCmd, String, String, String, String),
}
impl From<CmdVal> for Vec<String> {
fn from(cmd: CmdVal) -> Vec<String> {
match cmd {
CmdVal::Single(cmd) => vec![cmd.0.to_string()],
CmdVal::Double(cmd, arg) => vec![cmd.to_string(), arg],
CmdVal::Tripple(cmd, arg1, arg2) => vec![cmd.to_string(), arg1, arg2],
CmdVal::Quadruple(cmd, arg1, arg2, arg3) => vec![cmd.to_string(), arg1, arg2, arg3],
CmdVal::Quintuple(cmd, arg1, arg2, arg3, arg4) => {
vec![cmd.to_string(), arg1, arg2, arg3, arg4]
}
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)]
pub enum InMsgArgs {
StProp(PropKey, PropVal),
Cmd(CmdVal),
ObProp(PropKey),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct InMsg(pub InMsgFn, pub InMsgArgs);

View file

@ -2,8 +2,8 @@ pub mod player;
pub use player::Player;
pub mod communication;
pub use communication::{
CmdVal, InMsg, InMsgArgs, InMsgFn, PlayerEnded, PlayerEvent, PlayerProprChange, PlayerResponse,
PropKey, PropVal,
BoolProp, CmdVal, FpProp, InMsg, InMsgArgs, InMsgFn, PlayerEnded, PlayerEvent, PlayerProprChange,
PlayerResponse, PropKey, PropVal, StrProp,
};
#[cfg(test)]
mod communication_tests;

View file

@ -166,8 +166,72 @@ impl PartialUi for WebView {
try{console.log('Shell JS injected');if(window.self === window.top) {
window.qt={webChannelTransport:{send:window.chrome.webview.postMessage}};
window.chrome.webview.addEventListener('message',ev=>window.qt.webChannelTransport.onmessage(ev));
window.chrome.webview.addEventListener('message',ev=>{
try{
const data = typeof ev.data === "string" ? JSON.parse(ev.data) : ev.data;
if(data && data.__borderbreakerOverlay){
if(window.__bbShowOverlay){ window.__bbShowOverlay(data.__borderbreakerOverlay); }
return;
}
}catch(err){}
window.qt.webChannelTransport.onmessage(ev);
});
}}catch(e){}
try{
if(!document.getElementById('bb-overlay-style')){
const style = document.createElement('style');
style.id = 'bb-overlay-style';
style.textContent = `
#bbOverlayToast {
position: fixed;
top: 28px;
left: 50%;
transform: translateX(-50%) translateY(-8px);
padding: 10px 20px;
border-radius: 10px;
background: rgba(13, 15, 23, 0.92);
color: #fff;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.2px;
box-shadow: 0 12px 35px rgba(0,0,0,0.4);
z-index: 99999;
opacity: 0;
pointer-events: none;
transition: opacity .18s ease, transform .18s ease;
font-family: 'Segoe UI', 'Inter', sans-serif;
}
#bbOverlayToast.visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
`;
document.head.appendChild(style);
const toast = document.createElement('div');
toast.id = 'bbOverlayToast';
document.body.appendChild(toast);
}
window.__bbShowOverlay = (text) => {
const toast = document.getElementById('bbOverlayToast');
if(!toast) return;
toast.textContent = text;
toast.classList.add('visible');
clearTimeout(window.__bbOverlayTimer);
window.__bbOverlayTimer = setTimeout(() => toast.classList.remove('visible'), 1700);
};
}catch(e){}
try{
window.addEventListener('keydown', (e) => {
const tag = (e.target && e.target.tagName || "").toUpperCase();
if(e.ctrlKey || e.altKey || e.metaKey) return;
if(tag === 'INPUT' || tag === 'TEXTAREA') return;
if(e.target && e.target.isContentEditable) return;
if((e.key || "").toLowerCase() === "b") {
e.preventDefault();
window.chrome.webview.postMessage('{"id":1,"args":["borderbreaker-cycle"]}');
}
}, true);
}catch(e){}
"##, |_| Ok(())).expect("Cannot add script to webview");
Ok(())
}).expect("Cannot add content loading");
@ -177,16 +241,17 @@ impl PartialUi for WebView {
controller
.move_focus(webview2::MoveFocusReason::Programmatic)
.ok();
controller.add_accelerator_key_pressed(move |_, e| {
// Block F7, Ctrl+F, and Ctrl+G
let k = e.get_virtual_key()?;
if k == VK_F7 as u32 || k == 70 & 0x7F || k == 71 & 0x7F {
e.put_handled(true)
} else {
Ok(())
}
})
.unwrap();
controller
.add_accelerator_key_pressed(move |_, e| {
// Block F7, Ctrl+F, and Ctrl+G
let k = e.get_virtual_key()?;
if k == VK_F7 as u32 || k == 70 & 0x7F || k == 71 & 0x7F {
e.put_handled(true)
} else {
Ok(())
}
})
.unwrap();
controller_clone
.set(controller)

View file

@ -154,3 +154,13 @@ impl WindowStyle {
}
}
}
pub fn primary_monitor_ratio() -> f32 {
let width = unsafe { GetSystemMetrics(SM_CXSCREEN) } as f32;
let height = unsafe { GetSystemMetrics(SM_CYSCREEN) } as f32;
if height <= f32::EPSILON {
0.0
} else {
width / height
}
}