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 engine.log
/stremio*.exe engine.err
/target
/stremio*.exe
/libmpv-2.dll /libmpv-2.dll

View file

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

View file

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

View file

@ -166,8 +166,72 @@ impl PartialUi for WebView {
try{console.log('Shell JS injected');if(window.self === window.top) { try{console.log('Shell JS injected');if(window.self === window.top) {
window.qt={webChannelTransport:{send:window.chrome.webview.postMessage}}; 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){} }}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 script to webview");
Ok(()) Ok(())
}).expect("Cannot add content loading"); }).expect("Cannot add content loading");
@ -177,16 +241,17 @@ impl PartialUi for WebView {
controller controller
.move_focus(webview2::MoveFocusReason::Programmatic) .move_focus(webview2::MoveFocusReason::Programmatic)
.ok(); .ok();
controller.add_accelerator_key_pressed(move |_, e| { controller
// Block F7, Ctrl+F, and Ctrl+G .add_accelerator_key_pressed(move |_, e| {
let k = e.get_virtual_key()?; // Block F7, Ctrl+F, and Ctrl+G
if k == VK_F7 as u32 || k == 70 & 0x7F || k == 71 & 0x7F { let k = e.get_virtual_key()?;
e.put_handled(true) if k == VK_F7 as u32 || k == 70 & 0x7F || k == 71 & 0x7F {
} else { e.put_handled(true)
Ok(()) } else {
} Ok(())
}) }
.unwrap(); })
.unwrap();
controller_clone controller_clone
.set(controller) .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
}
}