mirror of
https://github.com/Stremio/stremio-shell-ng.git
synced 2026-04-21 07:41:56 +00:00
Add BorderBreaker aspect ratio controls
This commit is contained in:
parent
81fa5c902d
commit
2e66df3ad7
9 changed files with 919 additions and 504 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -1,3 +1,5 @@
|
||||||
/target
|
engine.log
|
||||||
/stremio*.exe
|
engine.err
|
||||||
|
/target
|
||||||
|
/stremio*.exe
|
||||||
/libmpv-2.dll
|
/libmpv-2.dll
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
289
src/stremio_app/aspect_ratio.rs
Normal file
289
src/stremio_app/aspect_ratio.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue