mirror of
https://github.com/Stremio/stremio-shell-ng.git
synced 2026-01-11 22:40:32 +00:00
commit
075b96df74
39 changed files with 162885 additions and 305 deletions
44
.github/workflows/build.yml
vendored
Normal file
44
.github/workflows/build.yml
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
name: Build Installer
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
# The Windows runners have autocrlf enabled by default
|
||||
# which causes failures for some of rustfmt's line-ending sensitive tests
|
||||
- name: disable git eol translation
|
||||
run: git config --global core.autocrlf false
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Rust Stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
- name: Build main application
|
||||
run: cargo build --release
|
||||
- name: Build the setup proggam
|
||||
run: |
|
||||
& 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' /DSIGN '/Sstremiosign=$qC:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x86\signtool.exe$q sign /f $q${{ github.workspace }}\certificates\smartcode-20211118-20241118.pfx$q /p ${{ secrets.WIN_CERT_PASSWORD }} /v $f' '${{ github.workspace }}\setup\Stremio.iss'
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
|
||||
aws-region: eu-west-1
|
||||
|
||||
- name: Upload to Amazon S3
|
||||
run: |
|
||||
aws s3 cp --acl public-read ".\StremioSetup-v$((get-item .\StremioSetup*.exe).VersionInfo.ProductVersion.Trim()).exe" s3://stremio-artifacts/stremio-shell-ng/${{ github.ref_name }}/
|
||||
- name: Generate RC descriptor
|
||||
run: |
|
||||
node ./generate_descriptor.js --wait-all --tag=${{ github.ref_name }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: stremio-setup
|
||||
path: StremioSetup-v*.exe
|
||||
27
.github/workflows/test.yml
vendored
Normal file
27
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
name: Continuous integration
|
||||
|
||||
on: [pull_request, push]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
# The Windows runners have autocrlf enabled by default
|
||||
# which causes failures for some of rustfmt's line-ending sensitive tests
|
||||
- name: disable git eol translation
|
||||
run: git config --global core.autocrlf false
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Stable with rustfmt and clippy
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
components: rustfmt, clippy
|
||||
- name: Lint code format
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Lint code
|
||||
run: cargo clippy --all -- -D warnings
|
||||
- name: Test
|
||||
run: cargo test
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
|||
/target
|
||||
/target
|
||||
/stremio*.exe
|
||||
1930
Cargo.lock
generated
1930
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
46
Cargo.toml
46
Cargo.toml
|
|
@ -1,14 +1,48 @@
|
|||
[package]
|
||||
name = "stremio-shell-ng"
|
||||
version = "0.1.0"
|
||||
version = "5.0.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
once_cell = "1.3.1"
|
||||
native-windows-gui = { version = "1.0.4", features = ["high-dpi"] }
|
||||
once_cell = "1.19"
|
||||
native-windows-gui = { version = "1", features = [
|
||||
"high-dpi",
|
||||
"notice",
|
||||
"tray-notification",
|
||||
"menu",
|
||||
] }
|
||||
native-windows-derive = "1"
|
||||
winapi = { version = "0.3.9", features = [
|
||||
"libloaderapi",
|
||||
"handleapi",
|
||||
"wincon",
|
||||
"winuser",
|
||||
"namedpipeapi"
|
||||
] }
|
||||
webview2 = "0.1.0"
|
||||
webview2-sys = "0.1.0-beta.1"
|
||||
mpv = "0.2.3"
|
||||
webview2 = "0.1.4"
|
||||
webview2-sys = "0.1.1"
|
||||
libmpv-sirno = "2.0.2-fork.1"
|
||||
libmpv-sys-sirno = "2.0.0-fork.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
clap = { version = "4", features = ["derive", "unicode"] }
|
||||
open = "5"
|
||||
urlencoding = "2"
|
||||
bitflags = "2"
|
||||
win32job = "2"
|
||||
parse-display = "0.9"
|
||||
flume = "0.11"
|
||||
whoami = "1.5"
|
||||
anyhow = "1"
|
||||
semver = "1"
|
||||
sha2 = "0.10"
|
||||
reqwest = { version = "0.12", features = ["stream", "json", "blocking"] }
|
||||
rand = "0.8"
|
||||
url = { version = "2", features = ["serde"] }
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
winres = "0.1"
|
||||
chrono = "0.4.22"
|
||||
[dev-dependencies]
|
||||
serde_test = "1.0.*"
|
||||
|
|
|
|||
BIN
bin/ffmpeg.exe
Normal file
BIN
bin/ffmpeg.exe
Normal file
Binary file not shown.
BIN
bin/ffprobe.exe
Normal file
BIN
bin/ffprobe.exe
Normal file
Binary file not shown.
BIN
bin/stremio-runtime.exe
Normal file
BIN
bin/stremio-runtime.exe
Normal file
Binary file not shown.
35
build.rs
Normal file
35
build.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use chrono::{Datelike, Local};
|
||||
use std::env;
|
||||
|
||||
extern crate winres;
|
||||
fn main() {
|
||||
let now = Local::now();
|
||||
let copyright = format!("Copyright © {} Smart Code OOD", now.year());
|
||||
let exe_name = format!("{}.exe", env::var("CARGO_PKG_NAME").unwrap());
|
||||
let mut res = winres::WindowsResource::new();
|
||||
res.set_manifest(
|
||||
r#"
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
</assembly>
|
||||
"#,
|
||||
);
|
||||
res.set("FileDescription", "Freedom to Stream");
|
||||
res.set("LegalCopyright", ©right);
|
||||
res.set("OriginalFilename", &exe_name);
|
||||
res.set_icon_with_id("images/stremio.ico", "MAINICON");
|
||||
res.append_rc_content(r##"SPLASHIMAGE IMAGE "images/stremio.png""##);
|
||||
res.compile().unwrap();
|
||||
}
|
||||
299
generate_descriptor.js
Normal file
299
generate_descriptor.js
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Copyright (C) 2017-2024 Smart Code OOD 203358507
|
||||
|
||||
// This script generates an auto update descriptor for a given tag.
|
||||
// It will get the files for the tag, calculate their hashes, and upload
|
||||
// the descriptor to S3. In order to run it, you need to have aws cli installed
|
||||
// and configured with your AWS credentials.
|
||||
|
||||
const { exec } = require("child_process");
|
||||
const { basename, posix } = require("path");
|
||||
const https = require("https");
|
||||
const { createHash } = require("crypto");
|
||||
const { tmpdir } = require("os");
|
||||
const fs = require("fs");
|
||||
const S3_BUCKET_PATH = "s3://stremio-artifacts/stremio-shell-ng/";
|
||||
const S3_VERSIONS_PATH = "s3://stremio-artifacts/stremio-shell-ng/[versions]/";
|
||||
const S3_VERSIONS_RC_PATH =
|
||||
"s3://stremio-artifacts/stremio-shell-ng/[versions]/rc/";
|
||||
const DOWNLOAD_ENDPOINT = "https://dl.strem.io/stremio-shell-ng/";
|
||||
const OS_EXT = {
|
||||
".exe": "windows",
|
||||
};
|
||||
const VERSION_REGEX = /^v(\d+\.\d+\.\d+).*$/;
|
||||
|
||||
const supportedArguments = Object.freeze({
|
||||
tag: {
|
||||
description:
|
||||
"The tag to generate the descriptor for. If not specified, the latest tag will be used. A tag must start with a v followed by the version number in semver format, followed by an optional suffix. For example: v1.2.3, v1.2.3-rc.1, v1.2.3-beta.2",
|
||||
parse: parseTagArgument,
|
||||
},
|
||||
force: {
|
||||
description: "Overwrite the descriptor if it already exists.",
|
||||
default: false,
|
||||
parse: parseBooleanArgument,
|
||||
},
|
||||
dry_run: {
|
||||
description:
|
||||
"Do not upload the descriptor to S3. Just print it to stdout. The descriptor is printed even if --quiet is set.",
|
||||
default: false,
|
||||
parse: parseBooleanArgument,
|
||||
},
|
||||
wait_all: {
|
||||
description:
|
||||
"By default at least one file is required to produce a descriptor. This flag will cause the script to exit (without error) if not all files are uploaded before generating the descriptor.",
|
||||
default: false,
|
||||
parse: parseBooleanArgument,
|
||||
},
|
||||
release: {
|
||||
description:
|
||||
"If this flag is set, the descriptor will be uploaded to the release path instead of the release candidate path.",
|
||||
default: false,
|
||||
parse: parseBooleanArgument,
|
||||
},
|
||||
quiet: {
|
||||
description: "Suppress all output except for errors.",
|
||||
default: false,
|
||||
parse: parseBooleanArgument,
|
||||
},
|
||||
help: {
|
||||
description: "Print this help message",
|
||||
parse: () => {
|
||||
usage();
|
||||
process.exit(0);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function parseBooleanArgument(value) {
|
||||
// Acceptable falsy values. Note that an empty string is considered truthy.
|
||||
// Thus --option and --option= will set option to true.
|
||||
return ["false", "0", "no", "off"].includes(value.toLowerCase())
|
||||
? false
|
||||
: true;
|
||||
}
|
||||
|
||||
function parseTagArgument(value) {
|
||||
if (value.match(VERSION_REGEX) !== null) return value;
|
||||
}
|
||||
|
||||
function usage() {
|
||||
log(`Usage: ${basename(process.argv[1])} [options]`);
|
||||
log("Options:");
|
||||
Object.keys(supportedArguments).forEach((key) => {
|
||||
log(
|
||||
` --${key.replace(/_/g, "-")}${typeof supportedArguments[key].default !== "undefined"
|
||||
? " [default: " + supportedArguments[key].default.toString() + "]"
|
||||
: ""
|
||||
}`
|
||||
);
|
||||
log(` ${supportedArguments[key].description}`);
|
||||
});
|
||||
}
|
||||
|
||||
const parseArguments = () => {
|
||||
const args = Object.keys(supportedArguments).reduce((acc, key) => {
|
||||
if (typeof supportedArguments[key].default !== "undefined")
|
||||
acc[key] = supportedArguments[key].default;
|
||||
return acc;
|
||||
}, {});
|
||||
try {
|
||||
for (let i = 2; i < process.argv.length; i++) {
|
||||
const arg = process.argv[i];
|
||||
if (arg.startsWith("--")) {
|
||||
// Stop processing arguments after --
|
||||
if (arg.length === 2) break;
|
||||
const eq_position = arg.indexOf("=");
|
||||
const name_end = eq_position === -1 ? arg.length : eq_position;
|
||||
const name = arg.slice(2, name_end).replace(/-/g, "_");
|
||||
if (!supportedArguments[name])
|
||||
throw new Error(`Unsupported argument ${arg}`);
|
||||
const value = supportedArguments[name].parse(arg.slice(name_end + 1));
|
||||
if (typeof value === "undefined")
|
||||
throw new Error(
|
||||
`Invalid value for argument --${name.replace(/_/g, "-")}`
|
||||
);
|
||||
args[name] = value;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
return args;
|
||||
};
|
||||
|
||||
const args = parseArguments();
|
||||
|
||||
function log(...params) {
|
||||
if (!args.quiet) console.info(...params);
|
||||
}
|
||||
|
||||
const s3Cmd = (command_line) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`aws s3 ${command_line}`, (err, stdout, stderr) => {
|
||||
const error = err || (stderr && new Error(stderr));
|
||||
if (error) return reject(error);
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const s3Ls = (path) => s3Cmd(`ls --no-paginate ${path}`).catch(() => { });
|
||||
const s3Cp = (src, dest) => s3Cmd(`cp --acl public-read ${src} ${dest}`);
|
||||
|
||||
// Downloads a file from S3 and returns a hash of it
|
||||
const s3Hash = (path) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = createHash("sha256");
|
||||
https
|
||||
.get(path, (res) => {
|
||||
res.on("data", hash.update.bind(hash));
|
||||
res.on("end", () => {
|
||||
resolve(hash.digest("hex"));
|
||||
});
|
||||
})
|
||||
.on("error", reject);
|
||||
});
|
||||
};
|
||||
|
||||
// Parses a line from the S3 listing
|
||||
const parseS3Listing = (tag) => (line) => {
|
||||
const path = (
|
||||
line.match(
|
||||
/^\s*(?<date>\d{4}(?:-\d{2}){2} \d\d(?::\d\d){2})\s+\d+\s+(?<name>.*)\s*$/
|
||||
) || {}
|
||||
).groups;
|
||||
if (!path) return;
|
||||
const os = OS_EXT[posix.extname(path.name)];
|
||||
if (!os) return;
|
||||
return {
|
||||
name: path.name,
|
||||
url: `${DOWNLOAD_ENDPOINT + tag}/${path.name}`,
|
||||
os,
|
||||
date: new Date(path.date),
|
||||
};
|
||||
};
|
||||
|
||||
const getFilesForTag = async (tag) =>
|
||||
(
|
||||
await s3Ls(S3_BUCKET_PATH + tag + "/").then((listing) => {
|
||||
log("Calculating hashes for files");
|
||||
return (listing || "").split("\n").map(parseS3Listing(tag));
|
||||
})
|
||||
).filter((file) => file);
|
||||
|
||||
const calculateFileChecksums = async (files) =>
|
||||
Promise.all(
|
||||
files.map(async (file) => {
|
||||
const checksum = await s3Hash(file.url);
|
||||
log("The hash for", file.name, "is", checksum);
|
||||
return {
|
||||
...file,
|
||||
checksum,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Generates the descriptor for a given tag
|
||||
// If no tag is provided, it will get the latest tag
|
||||
// An example descriptor:
|
||||
//
|
||||
// {
|
||||
// "version": "0.1.0",
|
||||
// "tag": "v0.1.0-new-setup",
|
||||
// "released": "2023-02-30T12:53:59.412Z",
|
||||
// "files": [
|
||||
// {
|
||||
// "name": "StremioSetup.exe",
|
||||
// "url": "https://s3-eu-west-1.amazonaws.com/stremio-artifacts/stremio-shell-ng/v0.1.0-new-setup/StremioSetup.exe",
|
||||
// "checksum": "0ff94905df4d94233d14f48ed68e31664a478a29204be4c7867c2389929c6ac3",
|
||||
// "os": "windows"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
const generateDescriptor = async (args) => {
|
||||
let tag = args.tag;
|
||||
if (!tag) {
|
||||
log("Obtaining the latest tag");
|
||||
tag = await s3Ls(S3_BUCKET_PATH).then((listing) => {
|
||||
// get the first line, remove the DIR prefix, and get the basename
|
||||
// which is the tag
|
||||
const first_path = listing.replace(/^\s+\w+\s+/gm, '').split('\n').find(line => line.match(VERSION_REGEX));
|
||||
return posix.basename(first_path);
|
||||
});
|
||||
}
|
||||
const desc_name = tag + ".json";
|
||||
const s3_rc_desc_path = S3_VERSIONS_RC_PATH + desc_name;
|
||||
const s3_dest_path = args.release
|
||||
? S3_VERSIONS_PATH + desc_name
|
||||
: s3_rc_desc_path;
|
||||
if (!args.force && await s3Ls(s3_dest_path).then((listing) => !!listing)) {
|
||||
throw new Error(
|
||||
`${args.release ? "" : "RC "}Descriptor for tag ${tag} already exists`
|
||||
);
|
||||
}
|
||||
if (
|
||||
args.release &&
|
||||
!args.force &&
|
||||
(await s3Ls(s3_rc_desc_path).then((listing) => !!listing))
|
||||
) {
|
||||
log(
|
||||
"Descriptor for tag",
|
||||
tag,
|
||||
"already exists in the RC folder. Moving it to the releases folder"
|
||||
);
|
||||
if (!args.dry_run) await s3Cp(s3_rc_desc_path, s3_dest_path);
|
||||
log("Done");
|
||||
return;
|
||||
}
|
||||
|
||||
log("Getting files for tag", tag);
|
||||
if (!tag) throw new Error("No tag found");
|
||||
const version = (tag.match(VERSION_REGEX) || [])[1];
|
||||
if (!version) throw new Error("No valid version found");
|
||||
const file_listing = await getFilesForTag(tag);
|
||||
// We need at least one file to extract the release date
|
||||
if (!file_listing.length) throw new Error("No files found");
|
||||
if (args.wait_all && file_listing.length < Object.keys(OS_EXT).length) {
|
||||
log(
|
||||
`Not all files are uploaded yet. Rerun this script after ${Object.keys(OS_EXT).length - file_listing.length
|
||||
} more are uploaded`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const files = await calculateFileChecksums(file_listing);
|
||||
const descriptor = {
|
||||
version,
|
||||
tag,
|
||||
released: file_listing[0].date.toISOString(),
|
||||
files,
|
||||
};
|
||||
const descriptor_text = JSON.stringify(descriptor, null, 2) + "\n";
|
||||
if (args.dry_run) {
|
||||
process.stdout.write(descriptor_text);
|
||||
return;
|
||||
}
|
||||
|
||||
const descriptor_path = `${tmpdir()}/${desc_name}`;
|
||||
log("Writting descriptor to", descriptor_path);
|
||||
fs.writeFileSync(descriptor_path, descriptor_text);
|
||||
|
||||
log(`Uploading ${args.release ? "" : "RC "}descriptor to S3`);
|
||||
try {
|
||||
await s3Cp(descriptor_path, s3_dest_path);
|
||||
} finally {
|
||||
// Clean up the temporary file even if the upload fails
|
||||
log("Cleaning up");
|
||||
fs.unlinkSync(descriptor_path);
|
||||
}
|
||||
log("Done");
|
||||
};
|
||||
|
||||
generateDescriptor(args).catch((err) => {
|
||||
console.error(err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
BIN
images/stremio.ico
Normal file
BIN
images/stremio.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
BIN
images/stremio.png
Normal file
BIN
images/stremio.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
images/stremio_gray.ico
Normal file
BIN
images/stremio_gray.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
BIN
images/windows-installer-header.bmp
Normal file
BIN
images/windows-installer-header.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
images/windows-installer.bmp
Normal file
BIN
images/windows-installer.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 201 KiB |
BIN
mpv.dll
BIN
mpv.dll
Binary file not shown.
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "stable-x86_64-pc-windows-msvc"
|
||||
157545
server.js
Normal file
157545
server.js
Normal file
File diff suppressed because one or more lines are too long
807
setup/CodeDependencies.iss
Normal file
807
setup/CodeDependencies.iss
Normal file
|
|
@ -0,0 +1,807 @@
|
|||
; -- CodeDependencies.iss --
|
||||
;
|
||||
; This script shows how to download and install any dependency such as .NET,
|
||||
; Visual C++ or SQL Server during your application's installation process.
|
||||
;
|
||||
; contribute: https://github.com/DomGries/InnoDependencyInstaller
|
||||
|
||||
|
||||
; -----------
|
||||
; SHARED CODE
|
||||
; -----------
|
||||
[Code]
|
||||
// types and variables
|
||||
type
|
||||
TDependency_Entry = record
|
||||
Filename: String;
|
||||
Parameters: String;
|
||||
Title: String;
|
||||
URL: String;
|
||||
Checksum: String;
|
||||
ForceSuccess: Boolean;
|
||||
RestartAfter: Boolean;
|
||||
end;
|
||||
|
||||
var
|
||||
Dependency_Memo: String;
|
||||
Dependency_List: array of TDependency_Entry;
|
||||
Dependency_NeedRestart, Dependency_ForceX86: Boolean;
|
||||
Dependency_DownloadPage: TDownloadWizardPage;
|
||||
|
||||
procedure Dependency_Add(const Filename, Parameters, Title, URL, Checksum: String; const ForceSuccess, RestartAfter: Boolean);
|
||||
var
|
||||
Dependency: TDependency_Entry;
|
||||
DependencyCount: Integer;
|
||||
begin
|
||||
Dependency_Memo := Dependency_Memo + #13#10 + '%1' + Title;
|
||||
|
||||
Dependency.Filename := Filename;
|
||||
Dependency.Parameters := Parameters;
|
||||
Dependency.Title := Title;
|
||||
|
||||
if FileExists(ExpandConstant('{tmp}{\}') + Filename) then begin
|
||||
Dependency.URL := '';
|
||||
end else begin
|
||||
Dependency.URL := URL;
|
||||
end;
|
||||
|
||||
Dependency.Checksum := Checksum;
|
||||
Dependency.ForceSuccess := ForceSuccess;
|
||||
Dependency.RestartAfter := RestartAfter;
|
||||
|
||||
DependencyCount := GetArrayLength(Dependency_List);
|
||||
SetArrayLength(Dependency_List, DependencyCount + 1);
|
||||
Dependency_List[DependencyCount] := Dependency;
|
||||
end;
|
||||
|
||||
<event('InitializeWizard')>
|
||||
procedure Dependency_Internal1;
|
||||
begin
|
||||
Dependency_DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil);
|
||||
end;
|
||||
|
||||
<event('PrepareToInstall')>
|
||||
function Dependency_Internal2(var NeedsRestart: Boolean): String;
|
||||
var
|
||||
DependencyCount, DependencyIndex, ResultCode: Integer;
|
||||
Retry: Boolean;
|
||||
TempValue: String;
|
||||
begin
|
||||
DependencyCount := GetArrayLength(Dependency_List);
|
||||
|
||||
if DependencyCount > 0 then begin
|
||||
Dependency_DownloadPage.Show;
|
||||
|
||||
for DependencyIndex := 0 to DependencyCount - 1 do begin
|
||||
if Dependency_List[DependencyIndex].URL <> '' then begin
|
||||
Dependency_DownloadPage.Clear;
|
||||
Dependency_DownloadPage.Add(Dependency_List[DependencyIndex].URL, Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Checksum);
|
||||
|
||||
Retry := True;
|
||||
while Retry do begin
|
||||
Retry := False;
|
||||
|
||||
try
|
||||
Dependency_DownloadPage.Download;
|
||||
except
|
||||
if Dependency_DownloadPage.AbortedByUser then begin
|
||||
Result := Dependency_List[DependencyIndex].Title;
|
||||
DependencyIndex := DependencyCount;
|
||||
end else begin
|
||||
case SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of
|
||||
IDABORT: begin
|
||||
Result := Dependency_List[DependencyIndex].Title;
|
||||
DependencyIndex := DependencyCount;
|
||||
end;
|
||||
IDRETRY: begin
|
||||
Retry := True;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
if Result = '' then begin
|
||||
for DependencyIndex := 0 to DependencyCount - 1 do begin
|
||||
Dependency_DownloadPage.SetText(Dependency_List[DependencyIndex].Title, '');
|
||||
Dependency_DownloadPage.SetProgress(DependencyIndex + 1, DependencyCount + 1);
|
||||
|
||||
while True do begin
|
||||
ResultCode := 0;
|
||||
if ShellExec('', ExpandConstant('{tmp}{\}') + Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Parameters, '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode) then begin
|
||||
if Dependency_List[DependencyIndex].RestartAfter then begin
|
||||
if DependencyIndex = DependencyCount - 1 then begin
|
||||
Dependency_NeedRestart := True;
|
||||
end else begin
|
||||
NeedsRestart := True;
|
||||
Result := Dependency_List[DependencyIndex].Title;
|
||||
end;
|
||||
break;
|
||||
end else if (ResultCode = 0) or Dependency_List[DependencyIndex].ForceSuccess then begin // ERROR_SUCCESS (0)
|
||||
break;
|
||||
end else if ResultCode = 1641 then begin // ERROR_SUCCESS_REBOOT_INITIATED (1641)
|
||||
NeedsRestart := True;
|
||||
Result := Dependency_List[DependencyIndex].Title;
|
||||
break;
|
||||
end else if ResultCode = 3010 then begin // ERROR_SUCCESS_REBOOT_REQUIRED (3010)
|
||||
Dependency_NeedRestart := True;
|
||||
break;
|
||||
end;
|
||||
end;
|
||||
|
||||
case SuppressibleMsgBox(FmtMessage(SetupMessage(msgErrorFunctionFailed), [Dependency_List[DependencyIndex].Title, IntToStr(ResultCode)]), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of
|
||||
IDABORT: begin
|
||||
Result := Dependency_List[DependencyIndex].Title;
|
||||
break;
|
||||
end;
|
||||
IDIGNORE: begin
|
||||
break;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
if Result <> '' then begin
|
||||
break;
|
||||
end;
|
||||
end;
|
||||
|
||||
if NeedsRestart then begin
|
||||
TempValue := '"' + ExpandConstant('{srcexe}') + '" /restart=1 /LANG="' + ExpandConstant('{language}') + '" /DIR="' + WizardDirValue + '" /GROUP="' + WizardGroupValue + '" /TYPE="' + WizardSetupType(False) + '" /COMPONENTS="' + WizardSelectedComponents(False) + '" /TASKS="' + WizardSelectedTasks(False) + '"';
|
||||
if WizardNoIcons then begin
|
||||
TempValue := TempValue + ' /NOICONS';
|
||||
end;
|
||||
RegWriteStringValue(HKA, 'SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', '{#SetupSetting("AppName")}', TempValue);
|
||||
end;
|
||||
end;
|
||||
|
||||
Dependency_DownloadPage.Hide;
|
||||
end;
|
||||
end;
|
||||
|
||||
<event('UpdateReadyMemo')>
|
||||
function Dependency_Internal3(const Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String;
|
||||
begin
|
||||
Result := '';
|
||||
if MemoUserInfoInfo <> '' then begin
|
||||
Result := Result + MemoUserInfoInfo + Newline + NewLine;
|
||||
end;
|
||||
if MemoDirInfo <> '' then begin
|
||||
Result := Result + MemoDirInfo + Newline + NewLine;
|
||||
end;
|
||||
if MemoTypeInfo <> '' then begin
|
||||
Result := Result + MemoTypeInfo + Newline + NewLine;
|
||||
end;
|
||||
if MemoComponentsInfo <> '' then begin
|
||||
Result := Result + MemoComponentsInfo + Newline + NewLine;
|
||||
end;
|
||||
if MemoGroupInfo <> '' then begin
|
||||
Result := Result + MemoGroupInfo + Newline + NewLine;
|
||||
end;
|
||||
if MemoTasksInfo <> '' then begin
|
||||
Result := Result + MemoTasksInfo;
|
||||
end;
|
||||
|
||||
if Dependency_Memo <> '' then begin
|
||||
if MemoTasksInfo = '' then begin
|
||||
Result := Result + SetupMessage(msgReadyMemoTasks);
|
||||
end;
|
||||
Result := Result + FmtMessage(Dependency_Memo, [Space]);
|
||||
end;
|
||||
end;
|
||||
|
||||
<event('NeedRestart')>
|
||||
function Dependency_Internal4: Boolean;
|
||||
begin
|
||||
Result := Dependency_NeedRestart;
|
||||
end;
|
||||
|
||||
function Dependency_IsX64: Boolean;
|
||||
begin
|
||||
Result := not Dependency_ForceX86 and Is64BitInstallMode;
|
||||
end;
|
||||
|
||||
function Dependency_String(const x86, x64: String): String;
|
||||
begin
|
||||
if Dependency_IsX64 then begin
|
||||
Result := x64;
|
||||
end else begin
|
||||
Result := x86;
|
||||
end;
|
||||
end;
|
||||
|
||||
function Dependency_ArchSuffix: String;
|
||||
begin
|
||||
Result := Dependency_String('', '_x64');
|
||||
end;
|
||||
|
||||
function Dependency_ArchTitle: String;
|
||||
begin
|
||||
Result := Dependency_String(' (x86)', ' (x64)');
|
||||
end;
|
||||
|
||||
function Dependency_IsNetCoreInstalled(const Version: String): Boolean;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
// source code: https://github.com/dotnet/deployment-tools/tree/master/src/clickonce/native/projects/NetCoreCheck
|
||||
if not FileExists(ExpandConstant('{tmp}{\}') + 'netcorecheck' + Dependency_ArchSuffix + '.exe') then begin
|
||||
ExtractTemporaryFile('netcorecheck' + Dependency_ArchSuffix + '.exe');
|
||||
end;
|
||||
Result := ShellExec('', ExpandConstant('{tmp}{\}') + 'netcorecheck' + Dependency_ArchSuffix + '.exe', Version, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0);
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet35;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-framework/net35-sp1
|
||||
if not IsDotNetInstalled(net35, 1) then begin
|
||||
Dependency_Add('dotnetfx35.exe',
|
||||
'/lang:enu /passive /norestart',
|
||||
'.NET Framework 3.5 Service Pack 1',
|
||||
'https://download.microsoft.com/download/2/0/E/20E90413-712F-438C-988E-FDAA79A8AC3D/dotnetfx35.exe',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet40;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-framework/net40
|
||||
if not IsDotNetInstalled(net4full, 0) then begin
|
||||
Dependency_Add('dotNetFx40_Full_setup.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Framework 4.0',
|
||||
'https://download.microsoft.com/download/1/B/E/1BE39E79-7E39-46A3-96FF-047F95396215/dotNetFx40_Full_setup.exe',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet45;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-framework/net452
|
||||
if not IsDotNetInstalled(net452, 0) then begin
|
||||
Dependency_Add('dotnetfx45.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Framework 4.5.2',
|
||||
'https://go.microsoft.com/fwlink/?LinkId=397707',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet46;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-framework/net462
|
||||
if not IsDotNetInstalled(net462, 0) then begin
|
||||
Dependency_Add('dotnetfx46.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Framework 4.6.2',
|
||||
'https://go.microsoft.com/fwlink/?linkid=780596',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet47;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-framework/net472
|
||||
if not IsDotNetInstalled(net472, 0) then begin
|
||||
Dependency_Add('dotnetfx47.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Framework 4.7.2',
|
||||
'https://go.microsoft.com/fwlink/?LinkId=863262',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet48;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-framework/net48
|
||||
if not IsDotNetInstalled(net48, 0) then begin
|
||||
Dependency_Add('dotnetfx48.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Framework 4.8',
|
||||
'https://go.microsoft.com/fwlink/?LinkId=2085155',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddNetCore31;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-core/3.1
|
||||
if not Dependency_IsNetCoreInstalled('Microsoft.NETCore.App 3.1.22') then begin
|
||||
Dependency_Add('netcore31' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Core Runtime 3.1.22' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/c2437aed-8cc4-41d0-a239-d6c7cf7bddae/062c37e8b06df740301c0bca1b0b7b9a/dotnet-runtime-3.1.22-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/4e95705e-1bb6-4764-b899-1b97eb70ea1d/dd311e073bd3e25b2efe2dcf02727e81/dotnet-runtime-3.1.22-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddNetCore31Asp;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-core/3.1
|
||||
if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App 3.1.22') then begin
|
||||
Dependency_Add('netcore31asp' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'ASP.NET Core Runtime 3.1.22' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/0a1a2ee5-b8ed-4f0d-a4af-a7bce9a9ac2b/d452039b49d79e8897f272c3ab34b875/aspnetcore-runtime-3.1.22-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/80e52143-31e8-450e-aa94-b3f8484aaba9/4b69e5c77d50e7b367960a0079c90a99/aspnetcore-runtime-3.1.22-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddNetCore31Desktop;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-core/3.1
|
||||
if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App 3.1.22') then begin
|
||||
Dependency_Add('netcore31desktop' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Desktop Runtime 3.1.22' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/e4fcd574-4487-4b4b-8ca8-c23177c6f59f/c6d67a04956169dc21895cdcb42bf344/windowsdesktop-runtime-3.1.22-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/1c14e24b-7f31-42dc-ba3c-83295a2d6f7e/41b93591162dfe556cc160ae44fbe75e/windowsdesktop-runtime-3.1.22-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet50;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/5.0
|
||||
if not Dependency_IsNetCoreInstalled('Microsoft.NETCore.App 5.0.13') then begin
|
||||
Dependency_Add('dotnet50' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Runtime 5.0.13' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/4a79fcd5-d61b-4606-8496-68071c8099c6/2bf770ca40521e8c4563072592eadd06/dotnet-runtime-5.0.13-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/fccf43d2-3e62-4ede-b5a5-592a7ccded7b/6339f1fdfe3317df5b09adf65f0261ab/dotnet-runtime-5.0.13-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet50Asp;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/5.0
|
||||
if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App 5.0.13') then begin
|
||||
Dependency_Add('dotnet50asp' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'ASP.NET Core Runtime 5.0.13' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/340f9482-fc43-4ef7-b434-e2ed57f55cb3/c641b805cef3823769409a6dbac5746b/aspnetcore-runtime-5.0.13-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/aac560f3-eac8-437e-aebd-9830119deb10/6a3880161cf527e4ec71f67efe4d91ad/aspnetcore-runtime-5.0.13-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet50Desktop;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/5.0
|
||||
if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App 5.0.13') then begin
|
||||
Dependency_Add('dotnet50desktop' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Desktop Runtime 5.0.13' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/c8125c6b-d399-4be3-b201-8f1394fc3b25/724758f754fc7b67daba74db8d6d91d9/windowsdesktop-runtime-5.0.13-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/2bfb80f2-b8f2-44b0-90c1-d3c8c1c8eac8/409dd3d3367feeeda048f4ff34b32e82/windowsdesktop-runtime-5.0.13-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet60;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/6.0
|
||||
if not Dependency_IsNetCoreInstalled('Microsoft.NETCore.App 6.0.8') then begin
|
||||
Dependency_Add('dotnet60' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Runtime 6.0.8' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/db70cd1d-4f33-4dc4-8293-57bb362175c7/5c27048a0fc814e58bc196666a9b0c61/dotnet-runtime-6.0.8-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/5af3de9d-1e5f-48ff-bfb7-f93c0957ffae/e8dd664b0439f4725f8c968e7aae7dd1/dotnet-runtime-6.0.8-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet60Asp;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/6.0
|
||||
if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App 6.0.8') then begin
|
||||
Dependency_Add('dotnet60asp' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'ASP.NET Core Runtime 6.0.8' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/26dd0df5-f2ef-4b47-8651-84a2496dd017/158f3a45dd0718fc3ceda10b56b22721/aspnetcore-runtime-6.0.8-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/f5ef50c0-4dd1-4301-857f-768834d5a006/852c6470e4e5f602eee280c1e4e4e4c3/aspnetcore-runtime-6.0.8-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet60Desktop;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/6.0
|
||||
if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App 6.0.8') then begin
|
||||
Dependency_Add('dotnet60desktop' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Desktop Runtime 6.0.8' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/61747fc6-7236-4d5e-85e5-a5df5f480f3a/02203594bf1331f0875aa6491419ffa1/windowsdesktop-runtime-6.0.8-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/b4a17a47-2fe8-498d-b817-30ad2e23f413/00020402af25ba40990c6cc3db5cb270/windowsdesktop-runtime-6.0.8-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddVC2005;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=26347
|
||||
if not IsMsiProductInstalled(Dependency_String('{86C9D5AA-F00C-4921-B3F2-C60AF92E2844}', '{A8D19029-8E5C-4E22-8011-48070F9E796E}'), PackVersionComponents(8, 0, 61000, 0)) then begin
|
||||
Dependency_Add('vcredist2005' + Dependency_ArchSuffix + '.exe',
|
||||
'/q',
|
||||
'Visual C++ 2005 Service Pack 1 Redistributable' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x86.EXE', 'https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x64.EXE'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddVC2008;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=26368
|
||||
if not IsMsiProductInstalled(Dependency_String('{DE2C306F-A067-38EF-B86C-03DE4B0312F9}', '{FDA45DDF-8E17-336F-A3ED-356B7B7C688A}'), PackVersionComponents(9, 0, 30729, 6161)) then begin
|
||||
Dependency_Add('vcredist2008' + Dependency_ArchSuffix + '.exe',
|
||||
'/q',
|
||||
'Visual C++ 2008 Service Pack 1 Redistributable' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x86.exe', 'https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddVC2010;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=26999
|
||||
if not IsMsiProductInstalled(Dependency_String('{1F4F1D2A-D9DA-32CF-9909-48485DA06DD5}', '{5B75F761-BAC8-33BC-A381-464DDDD813A3}'), PackVersionComponents(10, 0, 40219, 0)) then begin
|
||||
Dependency_Add('vcredist2010' + Dependency_ArchSuffix + '.exe',
|
||||
'/passive /norestart',
|
||||
'Visual C++ 2010 Service Pack 1 Redistributable' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe', 'https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddVC2012;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=30679
|
||||
if not IsMsiProductInstalled(Dependency_String('{4121ED58-4BD9-3E7B-A8B5-9F8BAAE045B7}', '{EFA6AFA1-738E-3E00-8101-FD03B86B29D1}'), PackVersionComponents(11, 0, 61030, 0)) then begin
|
||||
Dependency_Add('vcredist2012' + Dependency_ArchSuffix + '.exe',
|
||||
'/passive /norestart',
|
||||
'Visual C++ 2012 Update 4 Redistributable' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe', 'https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddVC2013;
|
||||
begin
|
||||
// https://support.microsoft.com/en-us/help/4032938
|
||||
if not IsMsiProductInstalled(Dependency_String('{B59F5BF1-67C8-3802-8E59-2CE551A39FC5}', '{20400CF0-DE7C-327E-9AE4-F0F38D9085F8}'), PackVersionComponents(12, 0, 40664, 0)) then begin
|
||||
Dependency_Add('vcredist2013' + Dependency_ArchSuffix + '.exe',
|
||||
'/passive /norestart',
|
||||
'Visual C++ 2013 Update 5 Redistributable' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/10912113/5da66ddebb0ad32ebd4b922fd82e8e25/vcredist_x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/10912041/cee5d6bca2ddbcd039da727bf4acb48a/vcredist_x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddVC2015To2022;
|
||||
begin
|
||||
// https://docs.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist
|
||||
if not IsMsiProductInstalled(Dependency_String('{65E5BD06-6392-3027-8C26-853107D3CF1A}', '{36F68A90-239C-34DF-B58C-64B30153CE35}'), PackVersionComponents(14, 30, 30704, 0)) then begin
|
||||
Dependency_Add('vcredist2022' + Dependency_ArchSuffix + '.exe',
|
||||
'/passive /norestart',
|
||||
'Visual C++ 2015-2022 Redistributable' + Dependency_ArchTitle,
|
||||
Dependency_String('https://aka.ms/vs/17/release/vc_redist.x86.exe', 'https://aka.ms/vs/17/release/vc_redist.x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDirectX;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=35
|
||||
Dependency_Add('dxwebsetup.exe',
|
||||
'/q',
|
||||
'DirectX Runtime',
|
||||
'https://download.microsoft.com/download/1/7/1/1718CCC4-6315-4D8E-9543-8E28A4E18C4C/dxwebsetup.exe',
|
||||
'', True, False);
|
||||
end;
|
||||
|
||||
procedure Dependency_AddSql2008Express;
|
||||
var
|
||||
Version: String;
|
||||
PackedVersion: Int64;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=30438
|
||||
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(10, 50, 4000, 0)) < 0) then begin
|
||||
Dependency_Add('sql2008express' + Dependency_ArchSuffix + '.exe',
|
||||
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
|
||||
'SQL Server 2008 R2 Service Pack 2 Express',
|
||||
Dependency_String('https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR_x64_ENU.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddSql2012Express;
|
||||
var
|
||||
Version: String;
|
||||
PackedVersion: Int64;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=56042
|
||||
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL11.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(11, 0, 7001, 0)) < 0) then begin
|
||||
Dependency_Add('sql2012express' + Dependency_ArchSuffix + '.exe',
|
||||
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
|
||||
'SQL Server 2012 Service Pack 4 Express',
|
||||
Dependency_String('https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR_x64_ENU.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddSql2014Express;
|
||||
var
|
||||
Version: String;
|
||||
PackedVersion: Int64;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=57473
|
||||
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL12.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(12, 0, 6024, 0)) < 0) then begin
|
||||
Dependency_Add('sql2014express' + Dependency_ArchSuffix + '.exe',
|
||||
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
|
||||
'SQL Server 2014 Service Pack 3 Express',
|
||||
Dependency_String('https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR_x64_ENU.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddSql2016Express;
|
||||
var
|
||||
Version: String;
|
||||
PackedVersion: Int64;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=56840
|
||||
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL13.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(13, 0, 5026, 0)) < 0) then begin
|
||||
Dependency_Add('sql2016express' + Dependency_ArchSuffix + '.exe',
|
||||
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
|
||||
'SQL Server 2016 Service Pack 2 Express',
|
||||
'https://download.microsoft.com/download/3/7/6/3767D272-76A1-4F31-8849-260BD37924E4/SQLServer2016-SSEI-Expr.exe',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddSql2017Express;
|
||||
var
|
||||
Version: String;
|
||||
PackedVersion: Int64;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=55994
|
||||
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL14.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(14, 0, 0, 0)) < 0) then begin
|
||||
Dependency_Add('sql2017express' + Dependency_ArchSuffix + '.exe',
|
||||
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
|
||||
'SQL Server 2017 Express',
|
||||
'https://download.microsoft.com/download/5/E/9/5E9B18CC-8FD5-467E-B5BF-BADE39C51F73/SQLServer2017-SSEI-Expr.exe',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddSql2019Express;
|
||||
var
|
||||
Version: String;
|
||||
PackedVersion: Int64;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=101064
|
||||
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(15, 0, 0, 0)) < 0) then begin
|
||||
Dependency_Add('sql2019express' + Dependency_ArchSuffix + '.exe',
|
||||
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
|
||||
'SQL Server 2019 Express',
|
||||
'https://download.microsoft.com/download/7/f/8/7f8a9c43-8c8a-4f7c-9f92-83c18d96b681/SQL2019-SSEI-Expr.exe',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddWebView2;
|
||||
begin
|
||||
if not RegValueExists(HKLM, Dependency_String('SOFTWARE', 'SOFTWARE\WOW6432Node') + '\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'pv') then begin
|
||||
Dependency_Add('MicrosoftEdgeWebview2Setup.exe',
|
||||
'/silent /install',
|
||||
'WebView2 Runtime',
|
||||
'https://go.microsoft.com/fwlink/p/?LinkId=2124703',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
[Setup]
|
||||
; -------------
|
||||
; EXAMPLE SETUP
|
||||
; -------------
|
||||
#ifndef Dependency_NoExampleSetup
|
||||
|
||||
; comment out dependency defines to disable installing them
|
||||
#define UseDotNet35
|
||||
#define UseDotNet40
|
||||
#define UseDotNet45
|
||||
#define UseDotNet46
|
||||
#define UseDotNet47
|
||||
#define UseDotNet48
|
||||
|
||||
; requires netcorecheck.exe and netcorecheck_x64.exe (see download link below)
|
||||
#define UseNetCoreCheck
|
||||
#ifdef UseNetCoreCheck
|
||||
#define UseNetCore31
|
||||
#define UseNetCore31Asp
|
||||
#define UseNetCore31Desktop
|
||||
#define UseDotNet50
|
||||
#define UseDotNet50Asp
|
||||
#define UseDotNet50Desktop
|
||||
#define UseDotNet60
|
||||
#define UseDotNet60Asp
|
||||
#define UseDotNet60Desktop
|
||||
#endif
|
||||
|
||||
#define UseVC2005
|
||||
#define UseVC2008
|
||||
#define UseVC2010
|
||||
#define UseVC2012
|
||||
#define UseVC2013
|
||||
#define UseVC2015To2022
|
||||
|
||||
; requires dxwebsetup.exe (see download link below)
|
||||
;#define UseDirectX
|
||||
|
||||
#define UseSql2008Express
|
||||
#define UseSql2012Express
|
||||
#define UseSql2014Express
|
||||
#define UseSql2016Express
|
||||
#define UseSql2017Express
|
||||
#define UseSql2019Express
|
||||
|
||||
#define UseWebView2
|
||||
|
||||
#define MyAppSetupName 'MyProgram'
|
||||
#define MyAppVersion '1.0'
|
||||
#define MyAppPublisher 'Inno Setup'
|
||||
#define MyAppCopyright 'Copyright © Inno Setup'
|
||||
#define MyAppURL 'https://jrsoftware.org/isinfo.php'
|
||||
|
||||
AppName={#MyAppSetupName}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppVerName={#MyAppSetupName} {#MyAppVersion}
|
||||
AppCopyright={#MyAppCopyright}
|
||||
VersionInfoVersion={#MyAppVersion}
|
||||
VersionInfoCompany={#MyAppPublisher}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
AppPublisherURL={#MyAppURL}
|
||||
AppSupportURL={#MyAppURL}
|
||||
AppUpdatesURL={#MyAppURL}
|
||||
OutputBaseFilename={#MyAppSetupName}-{#MyAppVersion}
|
||||
DefaultGroupName={#MyAppSetupName}
|
||||
DefaultDirName={autopf}\{#MyAppSetupName}
|
||||
UninstallDisplayIcon={app}\MyProgram.exe
|
||||
SourceDir=src
|
||||
OutputDir={#SourcePath}\bin
|
||||
AllowNoIcons=yes
|
||||
PrivilegesRequired=admin
|
||||
|
||||
; remove next line if you only deploy 32-bit binaries and dependencies
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
|
||||
[Languages]
|
||||
Name: en; MessagesFile: "compiler:Default.isl"
|
||||
Name: nl; MessagesFile: "compiler:Languages\Dutch.isl"
|
||||
Name: de; MessagesFile: "compiler:Languages\German.isl"
|
||||
|
||||
[Files]
|
||||
#ifdef UseNetCoreCheck
|
||||
; download netcorecheck.exe: https://go.microsoft.com/fwlink/?linkid=2135256
|
||||
; download netcorecheck_x64.exe: https://go.microsoft.com/fwlink/?linkid=2135504
|
||||
Source: "netcorecheck.exe"; Flags: dontcopy noencryption
|
||||
Source: "netcorecheck_x64.exe"; Flags: dontcopy noencryption
|
||||
#endif
|
||||
|
||||
#ifdef UseDirectX
|
||||
Source: "dxwebsetup.exe"; Flags: dontcopy noencryption
|
||||
#endif
|
||||
|
||||
Source: "MyProg-x64.exe"; DestDir: "{app}"; DestName: "MyProg.exe"; Check: Dependency_IsX64; Flags: ignoreversion
|
||||
Source: "MyProg.exe"; DestDir: "{app}"; Check: not Dependency_IsX64; Flags: ignoreversion
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#MyAppSetupName}"; Filename: "{app}\MyProg.exe"
|
||||
Name: "{group}\{cm:UninstallProgram,{#MyAppSetupName}}"; Filename: "{uninstallexe}"
|
||||
Name: "{commondesktop}\{#MyAppSetupName}"; Filename: "{app}\MyProg.exe"; Tasks: desktopicon
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\MyProg.exe"; Description: "{cm:LaunchProgram,{#MyAppSetupName}}"; Flags: nowait postinstall skipifsilent
|
||||
|
||||
[Code]
|
||||
function InitializeSetup: Boolean;
|
||||
begin
|
||||
#ifdef UseDotNet35
|
||||
Dependency_AddDotNet35;
|
||||
#endif
|
||||
#ifdef UseDotNet40
|
||||
Dependency_AddDotNet40;
|
||||
#endif
|
||||
#ifdef UseDotNet45
|
||||
Dependency_AddDotNet45;
|
||||
#endif
|
||||
#ifdef UseDotNet46
|
||||
Dependency_AddDotNet46;
|
||||
#endif
|
||||
#ifdef UseDotNet47
|
||||
Dependency_AddDotNet47;
|
||||
#endif
|
||||
#ifdef UseDotNet48
|
||||
Dependency_AddDotNet48;
|
||||
#endif
|
||||
|
||||
#ifdef UseNetCore31
|
||||
Dependency_AddNetCore31;
|
||||
#endif
|
||||
#ifdef UseNetCore31Asp
|
||||
Dependency_AddNetCore31Asp;
|
||||
#endif
|
||||
#ifdef UseNetCore31Desktop
|
||||
Dependency_AddNetCore31Desktop;
|
||||
#endif
|
||||
#ifdef UseDotNet50
|
||||
Dependency_AddDotNet50;
|
||||
#endif
|
||||
#ifdef UseDotNet50Asp
|
||||
Dependency_AddDotNet50Asp;
|
||||
#endif
|
||||
#ifdef UseDotNet50Desktop
|
||||
Dependency_AddDotNet50Desktop;
|
||||
#endif
|
||||
#ifdef UseDotNet60
|
||||
Dependency_AddDotNet60;
|
||||
#endif
|
||||
#ifdef UseDotNet60Asp
|
||||
Dependency_AddDotNet60Asp;
|
||||
#endif
|
||||
#ifdef UseDotNet60Desktop
|
||||
Dependency_AddDotNet60Desktop;
|
||||
#endif
|
||||
|
||||
#ifdef UseVC2005
|
||||
Dependency_AddVC2005;
|
||||
#endif
|
||||
#ifdef UseVC2008
|
||||
Dependency_AddVC2008;
|
||||
#endif
|
||||
#ifdef UseVC2010
|
||||
Dependency_AddVC2010;
|
||||
#endif
|
||||
#ifdef UseVC2012
|
||||
Dependency_AddVC2012;
|
||||
#endif
|
||||
#ifdef UseVC2013
|
||||
//Dependency_ForceX86 := True; // force 32-bit install of next dependencies
|
||||
Dependency_AddVC2013;
|
||||
//Dependency_ForceX86 := False; // disable forced 32-bit install again
|
||||
#endif
|
||||
#ifdef UseVC2015To2022
|
||||
Dependency_AddVC2015To2022;
|
||||
#endif
|
||||
|
||||
#ifdef UseDirectX
|
||||
ExtractTemporaryFile('dxwebsetup.exe');
|
||||
Dependency_AddDirectX;
|
||||
#endif
|
||||
|
||||
#ifdef UseSql2008Express
|
||||
Dependency_AddSql2008Express;
|
||||
#endif
|
||||
#ifdef UseSql2012Express
|
||||
Dependency_AddSql2012Express;
|
||||
#endif
|
||||
#ifdef UseSql2014Express
|
||||
Dependency_AddSql2014Express;
|
||||
#endif
|
||||
#ifdef UseSql2016Express
|
||||
Dependency_AddSql2016Express;
|
||||
#endif
|
||||
#ifdef UseSql2017Express
|
||||
Dependency_AddSql2017Express;
|
||||
#endif
|
||||
#ifdef UseSql2019Express
|
||||
Dependency_AddSql2019Express;
|
||||
#endif
|
||||
|
||||
#ifdef UseWebView2
|
||||
Dependency_AddWebView2;
|
||||
#endif
|
||||
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
#endif
|
||||
214
setup/Stremio.iss
Normal file
214
setup/Stremio.iss
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
; Script generated by the Inno Setup Script Wizard.
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
#define MyAppName "Stremio"
|
||||
#define MyAppExeName "stremio-shell-ng.exe"
|
||||
#define MyAppExeLocation SourcePath + "..\target\release\" + MyAppExeName
|
||||
#define MyAppVersion() GetVersionComponents(MyAppExeLocation, Local[0], Local[1], Local[2], Local[3]), \
|
||||
Str(Local[0]) + "." + Str(Local[1]) + "." + Str(Local[2])
|
||||
|
||||
#define MyAppPublisher "Smart Code OOD"
|
||||
#define MyAppCopyright "Copyright © " + GetDateTimeString('yyyy', '', '') + " " + MyAppPublisher
|
||||
#define MyAppURL "https://www.stremio.com/"
|
||||
#define MyAppGoodbyeURL "https://www.strem.io/goodbye"
|
||||
#define AssocTorrentExt ".torrent"
|
||||
#define AssocTorrentKey StringChange(MyAppName, " ", "") + AssocTorrentExt
|
||||
#define AssocTorrentDesc "Bittorrent seed file"
|
||||
|
||||
#define public Dependency_NoExampleSetup
|
||||
#include "CodeDependencies.iss"
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||
AppId={{DD3870DA-AF3C-4C73-B010-72944AB610C6}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
AppCopyright={#MyAppCopyright}
|
||||
AppPublisherURL={#MyAppURL}
|
||||
AppSupportURL={#MyAppURL}
|
||||
AppUpdatesURL={#MyAppURL}
|
||||
DefaultDirName={autopf}\{#MyAppName}
|
||||
SetupMutex=StremioShellNgSetupsMutex,Global\StremioShellNgSetupsMutex
|
||||
; Remove the following line to run in administrative install mode (install for all users.)
|
||||
PrivilegesRequired=lowest
|
||||
DisableReadyPage=yes
|
||||
DisableDirPage=yes
|
||||
DisableProgramGroupPage=yes
|
||||
; DisableFinishedPage=yes
|
||||
ChangesAssociations=yes
|
||||
OutputBaseFilename={#MyAppName}Setup-v{#MyAppVersion}
|
||||
OutputDir=..
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
LanguageDetectionMethod=uilanguage
|
||||
ShowLanguageDialog=auto
|
||||
CloseApplications=yes
|
||||
WizardImageFile={#SourcePath}..\images\windows-installer.bmp
|
||||
WizardSmallImageFile={#SourcePath}..\images\windows-installer-header.bmp
|
||||
SetupIconFile={#SourcePath}..\images\stremio.ico
|
||||
UninstallDisplayIcon={app}\{#MyAppExeName},0
|
||||
#ifdef SIGN
|
||||
SignTool=stremiosign
|
||||
SignedUninstaller=yes
|
||||
#endif
|
||||
|
||||
[Code]
|
||||
function InitializeSetup: Boolean;
|
||||
begin
|
||||
Dependency_AddWebView2;
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
function ShouldSkipPage(PageID: Integer): Boolean;
|
||||
begin
|
||||
{ Hide finish page if run app is selected }
|
||||
if (PageID = wpFinished) and WizardIsTaskSelected('runapp') then
|
||||
Result := True
|
||||
else
|
||||
Result := False;
|
||||
end;
|
||||
|
||||
procedure CurPageChanged(CurPageID: Integer);
|
||||
begin
|
||||
case (CurPageID) of
|
||||
wpSelectTasks: WizardForm.NextButton.Caption := SetupMessage(msgButtonInstall);
|
||||
wpFinished: WizardForm.NextButton.Caption := SetupMessage(msgButtonFinish);
|
||||
else
|
||||
WizardForm.NextButton.Caption := SetupMessage(msgButtonNext);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
if (CurStep = ssDone) and WizardIsTaskSelected('runapp') then
|
||||
ExecAsOriginalUser(ExpandConstant('{app}\{#MyAppExeName}'), '', '', SW_SHOW, ewNoWait, ResultCode);
|
||||
end;
|
||||
|
||||
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
|
||||
var
|
||||
ErrorCode: Integer;
|
||||
begin
|
||||
case (CurUninstallStep) of
|
||||
usPostUninstall: if MsgBox(ExpandConstant('{cm:RemoveDataFolder}'), mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then
|
||||
DelTree(ExpandConstant('{app}'), True, True, True);
|
||||
usDone: ShellExec('', ExpandConstant('{#MyAppGoodbyeURL}'), '', '', SW_SHOW, ewNoWait, ErrorCode);
|
||||
end;
|
||||
end;
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
Name: "armenian"; MessagesFile: "compiler:Languages\Armenian.isl"
|
||||
Name: "brazilianportuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl"
|
||||
Name: "bulgarian"; MessagesFile: "compiler:Languages\Bulgarian.isl"
|
||||
Name: "catalan"; MessagesFile: "compiler:Languages\Catalan.isl"
|
||||
Name: "corsican"; MessagesFile: "compiler:Languages\Corsican.isl"
|
||||
Name: "czech"; MessagesFile: "compiler:Languages\Czech.isl"
|
||||
Name: "danish"; MessagesFile: "compiler:Languages\Danish.isl"
|
||||
Name: "dutch"; MessagesFile: "compiler:Languages\Dutch.isl"
|
||||
Name: "finnish"; MessagesFile: "compiler:Languages\Finnish.isl"
|
||||
Name: "french"; MessagesFile: "compiler:Languages\French.isl"
|
||||
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
|
||||
Name: "hebrew"; MessagesFile: "compiler:Languages\Hebrew.isl"
|
||||
Name: "icelandic"; MessagesFile: "compiler:Languages\Icelandic.isl"
|
||||
Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl"
|
||||
Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl"
|
||||
Name: "norwegian"; MessagesFile: "compiler:Languages\Norwegian.isl"
|
||||
Name: "polish"; MessagesFile: "compiler:Languages\Polish.isl"
|
||||
Name: "portuguese"; MessagesFile: "compiler:Languages\Portuguese.isl"
|
||||
Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
|
||||
Name: "slovak"; MessagesFile: "compiler:Languages\Slovak.isl"
|
||||
Name: "slovenian"; MessagesFile: "compiler:Languages\Slovenian.isl"
|
||||
Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl"
|
||||
Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl"
|
||||
Name: "ukrainian"; MessagesFile: "compiler:Languages\Ukrainian.isl"
|
||||
|
||||
[CustomMessages]
|
||||
RemoveDataFolder=Remove all data and configuration?
|
||||
english.RemoveDataFolder=Remove all data and configuration?
|
||||
armenian.RemoveDataFolder=Հեռացնե՞լ բոլոր տվյալները և կոնֆիգուրացիան:
|
||||
brazilianportuguese.RemoveDataFolder=Remover todos os dados e configuração?
|
||||
bulgarian.RemoveDataFolder=Премахване на всички данни и конфигурация?
|
||||
catalan.RemoveDataFolder=Vols suprimir totes les dades i la configuració?
|
||||
corsican.RemoveDataFolder=Eliminate tutti i dati è a cunfigurazione?
|
||||
czech.RemoveDataFolder=Odebrat všechna data a konfiguraci?
|
||||
danish.RemoveDataFolder=Remove all data and configuration?
|
||||
dutch.RemoveDataFolder=Remove all data and configuration?
|
||||
finnish.RemoveDataFolder=Poistetaanko kaikki tiedot ja asetukset?
|
||||
french.RemoveDataFolder=Supprimer toutes les données et la configuration ?
|
||||
german.RemoveDataFolder=Alle Daten und Konfiguration entfernen?
|
||||
hebrew.RemoveDataFolder=Remove all data and configuration?
|
||||
icelandic.RemoveDataFolder=Fjarlægja öll gögn og stillingar?
|
||||
italian.RemoveDataFolder=Rimuovere tutti i dati e la configurazione?
|
||||
japanese.RemoveDataFolder=すべてのデータと構成を削除しますか?
|
||||
norwegian.RemoveDataFolder=Vil du fjerne all data og konfigurasjon?
|
||||
polish.RemoveDataFolder=Usunąć wszystkie dane i konfigurację?
|
||||
portuguese.RemoveDataFolder=Remover todos os dados e configuração?
|
||||
russian.RemoveDataFolder=Удалить все данные и конфигурацию?
|
||||
slovak.RemoveDataFolder=Chcete odstrániť všetky údaje a konfiguráciu?
|
||||
slovenian.RemoveDataFolder=Želite odstraniti vse podatke in konfiguracijo?
|
||||
spanish.RemoveDataFolder=¿Eliminar todos los datos y la configuración?
|
||||
turkish.RemoveDataFolder=Tüm veriler ve yapılandırma kaldırılsın mı?
|
||||
ukrainian.RemoveDataFolder=Видалити всі дані та конфігурацію?
|
||||
|
||||
[Tasks]
|
||||
Name: "runapp"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"
|
||||
;Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
Name: "assoctorrent"; Description: "Associate {#MyAppName} with .torrent files"
|
||||
|
||||
[Files]
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
Source: "{#MyAppExeLocation}"; DestDir: "{app}"; Flags: ignoreversion sign
|
||||
Source: "{#SourcePath}..\mpv.dll"; DestDir: "{app}"; Flags: ignoreversion sign
|
||||
Source: "{#SourcePath}..\bin\ffmpeg.exe"; DestDir: "{app}"; Flags: ignoreversion sign
|
||||
Source: "{#SourcePath}..\bin\ffprobe.exe"; DestDir: "{app}"; Flags: ignoreversion sign
|
||||
Source: "{#SourcePath}..\bin\stremio-runtime.exe"; DestDir: "{app}"; Flags: ignoreversion sign
|
||||
Source: "{#SourcePath}..\server.js"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
[Registry]
|
||||
; Associate .torrent files if assoctorrent task is selected
|
||||
Root: HKA; Subkey: "Software\Classes\{#AssocTorrentExt}}\OpenWithProgids"; ValueType: string; ValueName: "{#AssocTorrentKey}"; ValueData: ""; Flags: uninsdeletevalue; Tasks: assoctorrent
|
||||
Root: HKA; Subkey: "Software\Classes\{#AssocTorrentKey}"; ValueType: string; ValueName: ""; ValueData: "{#AssocTorrentDesc}"; Flags: uninsdeletekey; Tasks: assoctorrent
|
||||
Root: HKA; Subkey: "Software\Classes\{#AssocTorrentKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey; Tasks: assoctorrent
|
||||
Root: HKA; Subkey: "Software\Classes\{#AssocTorrentKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey; Tasks: assoctorrent
|
||||
|
||||
; stremio: protocol
|
||||
Root: HKA; Subkey: "Software\Classes\stremio"; ValueType: string; ValueName: ""; ValueData: "URL:Stremio Protocol"; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\stremio"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\stremio\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\stremio\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey
|
||||
|
||||
; magnet: protocol
|
||||
Root: HKA; Subkey: "Software\Classes\magnet"; ValueType: string; ValueName: ""; ValueData: "URL:BitTorrent magnet"; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\magnet"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\magnet\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\magnet\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey
|
||||
|
||||
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".torrent"; ValueData: ""; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".avi"; ValueData: ""; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".asf"; ValueData: ""; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".mkv"; ValueData: ""; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".mp4"; ValueData: ""; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".mov"; ValueData: ""; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".ogg"; ValueData: ""; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".ogv"; ValueData: ""; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".wmv"; ValueData: ""; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".srt"; ValueData: ""; Flags: uninsdeletekey
|
||||
|
||||
[Icons]
|
||||
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
; This is used if the desktop shortcut is created by the [run] section.
|
||||
; [UninstallDelete]
|
||||
; Type: files; Name: "{autodesktop}\{#MyAppName}.lnk"
|
||||
|
||||
; We don't use the run section as the .torrent association is very hard to handle
|
||||
; [Run]
|
||||
; Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||
; Filename: "cmd"; Parameters: "/c copy ""{autoprograms}\{#MyAppName}.lnk"" ""{autodesktop}"""; Description: "{cm:CreateDesktopIcon}"; Flags: postinstall skipifsilent shellexec runhidden waituntilterminated runascurrentuser
|
||||
12
setup/create_setup.bat
Normal file
12
setup/create_setup.bat
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
@echo off
|
||||
set mypath=%~dp0
|
||||
|
||||
:: Compile the main executable
|
||||
if not exist "%mypath%..\target\release\stremio-shell-ng.exe" (
|
||||
cargo build --release
|
||||
) else (
|
||||
echo Main executable is already built
|
||||
)
|
||||
|
||||
:: Compile the installer
|
||||
"C:\Program Files (x86)\Inno Setup 6\ISCC.exe" "%mypath%Stremio.iss"
|
||||
217
src/main.rs
217
src/main.rs
|
|
@ -1,9 +1,45 @@
|
|||
use native_windows_gui::{self as nwg, Window};
|
||||
use once_cell::unsync::OnceCell;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
use webview2::Controller;
|
||||
use winapi::um::winuser::*;
|
||||
#![cfg_attr(all(not(test), not(debug_assertions)), windows_subsystem = "windows")]
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
use std::{io::Write, path::Path, process::exit};
|
||||
use url::Url;
|
||||
use whoami::username;
|
||||
|
||||
use clap::Parser;
|
||||
use native_windows_gui::{self as nwg, NativeUi};
|
||||
mod stremio_app;
|
||||
use crate::stremio_app::{
|
||||
constants::{DEV_ENDPOINT, IPC_PATH, STA_ENDPOINT, WEB_ENDPOINT},
|
||||
stremio_server::StremioServer,
|
||||
MainWindow, PipeClient,
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(version)]
|
||||
struct Opt {
|
||||
command: Option<String>,
|
||||
#[clap(
|
||||
long,
|
||||
help = "Start the app only in system tray and keep the window hidden"
|
||||
)]
|
||||
start_hidden: bool,
|
||||
#[clap(long, help = "Enable dev tools when pressing F12")]
|
||||
dev_tools: bool,
|
||||
#[clap(long, help = "Use software rendering for the webview")]
|
||||
disable_gpu: bool,
|
||||
#[clap(long, help = "Disable the server and load the WebUI from localhost")]
|
||||
development: bool,
|
||||
#[clap(long, help = "Shortcut for --webui-url=https://staging.strem.io/")]
|
||||
staging: bool,
|
||||
#[clap(long, default_value = WEB_ENDPOINT, help = "Override the WebUI URL")]
|
||||
webui_url: String,
|
||||
#[clap(long, help = "Ovveride autoupdater endpoint")]
|
||||
autoupdater_endpoint: Option<Url>,
|
||||
#[clap(long, help = "Forces reinstalling current version")]
|
||||
force_update: bool,
|
||||
#[clap(long, help = "Check for RC updates")]
|
||||
release_candidate: bool,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// native-windows-gui has some basic high DPI support with the high-dpi
|
||||
|
|
@ -12,129 +48,60 @@ fn main() {
|
|||
//
|
||||
// Use an application manifest to get rid of this deprecated warning.
|
||||
#[allow(deprecated)]
|
||||
unsafe { nwg::set_dpi_awareness() };
|
||||
unsafe {
|
||||
nwg::set_dpi_awareness()
|
||||
};
|
||||
nwg::enable_visual_styles();
|
||||
|
||||
nwg::init().unwrap();
|
||||
let opt = Opt::parse();
|
||||
|
||||
let mut window = Window::default();
|
||||
let dimensions = (1600, 900);
|
||||
let [total_width, total_height] = [nwg::Monitor::width(), nwg::Monitor::height()];
|
||||
let x = (total_width-dimensions.0)/2;
|
||||
let y = (total_height-dimensions.1)/2;
|
||||
Window::builder()
|
||||
.title("Stremio")
|
||||
.size(dimensions)
|
||||
.position((x, y))
|
||||
.build(&mut window)
|
||||
.unwrap();
|
||||
|
||||
let window_handle = window.handle;
|
||||
let hwnd = window_handle.hwnd().expect("unable to obtain hwnd");
|
||||
|
||||
// Initialize mpv
|
||||
let mut mpv_builder = mpv::MpvHandlerBuilder::new()
|
||||
.expect("Error while creating MPV builder");
|
||||
mpv_builder.set_option("wid", hwnd as i64).expect("failed setting wid");
|
||||
//mpv_builder.set_option("vo", "gpu").expect("unable to set vo");
|
||||
// win, opengl: works but least performancy, 10-15% CPU
|
||||
// winvk, vulkan: works as good as d3d11
|
||||
// d3d11, d3d11: works great
|
||||
// dxinterop, auto: works, slightly more cpu use than d3d11
|
||||
// angle, gpu-api: seems to have almost no effect on performance
|
||||
// default (auto) seems to be d3d11 (vo/gpu/d3d11)
|
||||
/*
|
||||
mpv_builder.set_option("gpu-context", "angle")
|
||||
.and_then(|_| mpv_builder.set_option("gpu-api", "auto"))
|
||||
.expect("setting gpu options failed");
|
||||
*/
|
||||
mpv_builder.try_hardware_decoding()
|
||||
.expect("failed setting hwdec");
|
||||
mpv_builder.set_option("terminal", "yes").expect("failed setting terminal");
|
||||
mpv_builder.set_option("msg-level", "all=v").expect("failed setting msg-level");
|
||||
//mpv_builder.set_option("quiet", "yes").expect("failed setting msg-level");
|
||||
let mut mpv = mpv_builder
|
||||
.build()
|
||||
.expect("Error while initializing MPV with opengl");
|
||||
//let video_path = "/home/ivo/storage/bbb_sunflower_1080p_30fps_normal.mp4";
|
||||
let video_path = "http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_30fps_normal.mp4";
|
||||
mpv.command(&["loadfile", video_path])
|
||||
.expect("Error loading file");
|
||||
|
||||
let controller: Rc<OnceCell<Controller>> = Rc::new(OnceCell::new());
|
||||
let controller_clone = controller.clone();
|
||||
let result = webview2::Environment::builder().build(move |env| {
|
||||
env.unwrap()
|
||||
.create_controller(hwnd, move |c| {
|
||||
let c = c.unwrap();
|
||||
|
||||
if let Ok(c2) = c.get_controller2() {
|
||||
c2.put_default_background_color(webview2_sys::Color {
|
||||
r: 255,
|
||||
g: 255,
|
||||
b: 255,
|
||||
a: 0,
|
||||
})
|
||||
.unwrap();
|
||||
} else {
|
||||
eprintln!("failed to get interface to controller2");
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let mut rect = mem::zeroed();
|
||||
GetClientRect(hwnd, &mut rect);
|
||||
c.put_bounds(rect).unwrap();
|
||||
}
|
||||
|
||||
let webview = c.get_webview().unwrap();
|
||||
webview.navigate("https://www.boyanpetrov.rip/stremio/index.html").unwrap();
|
||||
|
||||
controller_clone.set(c).unwrap();
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
if let Err(e) = result {
|
||||
nwg::modal_fatal_message(
|
||||
&window_handle,
|
||||
"Failed to Create WebView2 Environment",
|
||||
&format!("{}", e),
|
||||
);
|
||||
}
|
||||
let window_handle = window.handle;
|
||||
|
||||
// There isn't an OnWindowRestored event for SC_RESTORE in
|
||||
// native-windows-gui, so we use raw events.
|
||||
nwg::bind_raw_event_handler(&window_handle, 0xffff + 1, move |_, msg, w, _| {
|
||||
match (msg, w as usize) {
|
||||
(WM_SIZE, _) => {
|
||||
if let Some(controller) = controller.get() {
|
||||
unsafe {
|
||||
let mut rect = mem::zeroed();
|
||||
GetClientRect(window_handle.hwnd().unwrap(), &mut rect);
|
||||
controller.put_bounds(rect).unwrap();
|
||||
}
|
||||
}
|
||||
let command = match opt.command {
|
||||
Some(file) => {
|
||||
if Path::new(&file).exists() {
|
||||
"file:///".to_string() + &file.replace('\\', "/")
|
||||
} else {
|
||||
file
|
||||
}
|
||||
(WM_MOVE, _) => {
|
||||
if let Some(controller) = controller.get() {
|
||||
controller.notify_parent_window_position_changed().unwrap();
|
||||
}
|
||||
}
|
||||
(WM_SYSCOMMAND, SC_MINIMIZE) => {
|
||||
if let Some(controller) = controller.get() {
|
||||
controller.put_is_visible(false).unwrap();
|
||||
}
|
||||
}
|
||||
(WM_SYSCOMMAND, SC_RESTORE) => {
|
||||
if let Some(controller) = controller.get() {
|
||||
controller.put_is_visible(true).unwrap();
|
||||
}
|
||||
}
|
||||
(WM_CLOSE, _) => nwg::stop_thread_dispatch(),
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
})
|
||||
.unwrap();
|
||||
None => "".to_string(),
|
||||
};
|
||||
|
||||
// Single application IPC
|
||||
let mut commands_path = IPC_PATH.to_string();
|
||||
// Append the username so it works per User
|
||||
commands_path.push_str(&username());
|
||||
let socket_path = Path::new(&commands_path);
|
||||
if let Ok(mut stream) = PipeClient::connect(socket_path) {
|
||||
stream.write_all(command.as_bytes()).ok();
|
||||
exit(0);
|
||||
}
|
||||
// END IPC
|
||||
|
||||
if !opt.development {
|
||||
StremioServer::new();
|
||||
}
|
||||
|
||||
let webui_url = if opt.development && opt.webui_url == WEB_ENDPOINT {
|
||||
DEV_ENDPOINT.to_string()
|
||||
} else if opt.staging && opt.webui_url == WEB_ENDPOINT {
|
||||
STA_ENDPOINT.to_string()
|
||||
} else {
|
||||
opt.webui_url
|
||||
};
|
||||
|
||||
nwg::init().expect("Failed to init Native Windows GUI");
|
||||
let _app = MainWindow::build_ui(MainWindow {
|
||||
command,
|
||||
commands_path: Some(commands_path),
|
||||
webui_url,
|
||||
dev_tools: opt.development || opt.dev_tools,
|
||||
disable_gpu: opt.disable_gpu,
|
||||
start_hidden: opt.start_hidden,
|
||||
autoupdater_endpoint: opt.autoupdater_endpoint,
|
||||
force_update: opt.force_update,
|
||||
release_candidate: opt.release_candidate,
|
||||
..Default::default()
|
||||
})
|
||||
.expect("Failed to build UI");
|
||||
nwg::dispatch_thread_events();
|
||||
}
|
||||
|
|
|
|||
369
src/stremio_app/app.rs
Normal file
369
src/stremio_app/app.rs
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
use native_windows_derive::NwgUi;
|
||||
use native_windows_gui as nwg;
|
||||
use rand::Rng;
|
||||
use serde_json;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
io::Read,
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Command},
|
||||
str,
|
||||
sync::{Arc, Mutex},
|
||||
thread, time,
|
||||
};
|
||||
use url::Url;
|
||||
use winapi::um::winuser::WS_EX_TOPMOST;
|
||||
|
||||
use crate::stremio_app::{
|
||||
constants::{APP_NAME, UPDATE_ENDPOINT, UPDATE_INTERVAL, WINDOW_MIN_HEIGHT, WINDOW_MIN_WIDTH},
|
||||
ipc::{RPCRequest, RPCResponse},
|
||||
splash::SplashImage,
|
||||
stremio_player::Player,
|
||||
stremio_wevbiew::WebView,
|
||||
systray::SystemTray,
|
||||
updater,
|
||||
window_helper::WindowStyle,
|
||||
PipeServer,
|
||||
};
|
||||
|
||||
#[derive(Default, NwgUi)]
|
||||
pub struct MainWindow {
|
||||
pub command: String,
|
||||
pub commands_path: Option<String>,
|
||||
pub webui_url: String,
|
||||
pub dev_tools: bool,
|
||||
pub disable_gpu: bool,
|
||||
pub start_hidden: bool,
|
||||
pub autoupdater_endpoint: Option<Url>,
|
||||
pub force_update: bool,
|
||||
pub release_candidate: bool,
|
||||
pub autoupdater_setup_file: Arc<Mutex<Option<PathBuf>>>,
|
||||
pub saved_window_style: RefCell<WindowStyle>,
|
||||
#[nwg_resource]
|
||||
pub embed: nwg::EmbedResource,
|
||||
#[nwg_resource(source_embed: Some(&data.embed), source_embed_str: Some("MAINICON"))]
|
||||
pub window_icon: nwg::Icon,
|
||||
#[nwg_control(icon: Some(&data.window_icon), title: APP_NAME, flags: "MAIN_WINDOW")]
|
||||
#[nwg_events( OnWindowClose: [Self::on_quit(SELF, EVT_DATA)], OnInit: [Self::on_init], OnPaint: [Self::on_paint], OnMinMaxInfo: [Self::on_min_max(SELF, EVT_DATA)], OnWindowMinimize: [Self::transmit_window_state_change], OnWindowMaximize: [Self::transmit_window_state_change] )]
|
||||
pub window: nwg::Window,
|
||||
#[nwg_partial(parent: window)]
|
||||
#[nwg_events((tray_exit, OnMenuItemSelected): [nwg::stop_thread_dispatch()], (tray_show_hide, OnMenuItemSelected): [Self::on_show_hide], (tray_topmost, OnMenuItemSelected): [Self::on_toggle_topmost]) ]
|
||||
pub tray: SystemTray,
|
||||
#[nwg_partial(parent: window)]
|
||||
pub webview: WebView,
|
||||
#[nwg_partial(parent: window)]
|
||||
pub player: Player,
|
||||
#[nwg_partial(parent: window)]
|
||||
pub splash_screen: SplashImage,
|
||||
#[nwg_control]
|
||||
#[nwg_events(OnNotice: [Self::on_toggle_fullscreen_notice] )]
|
||||
pub toggle_fullscreen_notice: nwg::Notice,
|
||||
#[nwg_control]
|
||||
#[nwg_events(OnNotice: [nwg::stop_thread_dispatch()] )]
|
||||
pub quit_notice: nwg::Notice,
|
||||
#[nwg_control]
|
||||
#[nwg_events(OnNotice: [Self::on_hide_splash_notice] )]
|
||||
pub hide_splash_notice: nwg::Notice,
|
||||
#[nwg_control]
|
||||
#[nwg_events(OnNotice: [Self::on_focus_notice] )]
|
||||
pub focus_notice: nwg::Notice,
|
||||
}
|
||||
|
||||
impl MainWindow {
|
||||
fn transmit_window_full_screen_change(&self, prevent_close: bool) {
|
||||
let web_channel = self.webview.channel.borrow();
|
||||
let (web_tx, _) = web_channel
|
||||
.as_ref()
|
||||
.expect("Cannont obtain communication channel for the Web UI");
|
||||
let web_tx_app = web_tx.clone();
|
||||
let full_screen = {
|
||||
self.saved_window_style
|
||||
.try_borrow()
|
||||
.ok()
|
||||
.map(|saved_style| saved_style.full_screen)
|
||||
};
|
||||
if let Some(full_screen) = full_screen {
|
||||
web_tx_app
|
||||
.send(RPCResponse::visibility_change(
|
||||
self.window.visible(),
|
||||
prevent_close as u32,
|
||||
full_screen,
|
||||
))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
fn transmit_window_state_change(&self) {
|
||||
if let (Some(hwnd), Ok(web_channel), Ok(style)) = (
|
||||
self.window.handle.hwnd(),
|
||||
self.webview.channel.try_borrow(),
|
||||
self.saved_window_style.try_borrow(),
|
||||
) {
|
||||
let state = style.clone().get_window_state(hwnd);
|
||||
drop(style);
|
||||
let (web_tx, _) = web_channel
|
||||
.as_ref()
|
||||
.expect("Cannont obtain communication channel for the Web UI");
|
||||
let web_tx_app = web_tx.clone();
|
||||
web_tx_app.send(RPCResponse::state_change(state)).ok();
|
||||
} else {
|
||||
eprintln!("Cannot obtain window handle or communication channel");
|
||||
}
|
||||
}
|
||||
fn on_init(&self) {
|
||||
self.webview.endpoint.set(self.webui_url.clone()).ok();
|
||||
self.webview.dev_tools.set(self.dev_tools).ok();
|
||||
self.webview.disable_gpu.set(self.disable_gpu).ok();
|
||||
if let Some(hwnd) = self.window.handle.hwnd() {
|
||||
if let Ok(mut saved_style) = self.saved_window_style.try_borrow_mut() {
|
||||
saved_style.center_window(hwnd, WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
self.window.set_visible(!self.start_hidden);
|
||||
self.tray.tray_show_hide.set_checked(!self.start_hidden);
|
||||
|
||||
let player_channel = self.player.channel.borrow();
|
||||
let (player_tx, player_rx) = player_channel
|
||||
.as_ref()
|
||||
.expect("Cannont obtain communication channel for the Player");
|
||||
let player_tx = player_tx.clone();
|
||||
let player_rx = player_rx.clone();
|
||||
|
||||
let web_channel = self.webview.channel.borrow();
|
||||
let (web_tx, web_rx) = web_channel
|
||||
.as_ref()
|
||||
.expect("Cannont obtain communication channel for the Web UI");
|
||||
let web_tx_player = web_tx.clone();
|
||||
let web_tx_web = web_tx.clone();
|
||||
let web_tx_arg = web_tx.clone();
|
||||
let web_tx_upd = web_tx.clone();
|
||||
let web_rx = web_rx.clone();
|
||||
let command_clone = self.command.clone();
|
||||
|
||||
// Single application IPC
|
||||
let socket_path = Path::new(
|
||||
self.commands_path
|
||||
.as_ref()
|
||||
.expect("Cannot initialie the single application IPC"),
|
||||
);
|
||||
|
||||
let autoupdater_endpoint = self.autoupdater_endpoint.clone();
|
||||
let force_update = self.force_update;
|
||||
let release_candidate = self.release_candidate;
|
||||
let autoupdater_setup_file = self.autoupdater_setup_file.clone();
|
||||
thread::spawn(move || loop {
|
||||
let current_version = env!("CARGO_PKG_VERSION")
|
||||
.parse()
|
||||
.expect("Should always be valid");
|
||||
let updater_endpoint = if let Some(ref endpoint) = autoupdater_endpoint {
|
||||
endpoint.clone()
|
||||
} else {
|
||||
let mut rng = rand::thread_rng();
|
||||
let index = rng.gen_range(0..UPDATE_ENDPOINT.len());
|
||||
let mut url = Url::parse(UPDATE_ENDPOINT[index]).unwrap();
|
||||
if release_candidate {
|
||||
url.query_pairs_mut().append_pair("rc", "true");
|
||||
}
|
||||
url
|
||||
};
|
||||
let updater = updater::Updater::new(current_version, &updater_endpoint, force_update);
|
||||
|
||||
match updater.autoupdate() {
|
||||
Ok(Some(update)) => {
|
||||
println!("New version ready to install v{}", update.version);
|
||||
let mut autoupdater_setup_file = autoupdater_setup_file.lock().unwrap();
|
||||
*autoupdater_setup_file = Some(update.file.clone());
|
||||
web_tx_upd.send(RPCResponse::update_available()).ok();
|
||||
}
|
||||
Ok(None) => println!("No new updates found"),
|
||||
Err(e) => eprintln!("Failed to fetch updates: {e}"),
|
||||
}
|
||||
|
||||
thread::sleep(time::Duration::from_secs(UPDATE_INTERVAL));
|
||||
}); // thread
|
||||
|
||||
if let Ok(mut listener) = PipeServer::bind(socket_path) {
|
||||
thread::spawn(move || loop {
|
||||
if let Ok(mut stream) = listener.accept() {
|
||||
let mut buf = vec![];
|
||||
stream.read_to_end(&mut buf).ok();
|
||||
if let Ok(s) = str::from_utf8(&buf) {
|
||||
// ['open-media', url]
|
||||
web_tx_arg.send(RPCResponse::open_media(s.to_string())).ok();
|
||||
println!("{}", s);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Read message from player
|
||||
thread::spawn(move || loop {
|
||||
player_rx
|
||||
.iter()
|
||||
.map(|msg| web_tx_player.send(msg))
|
||||
.for_each(drop);
|
||||
}); // thread
|
||||
|
||||
let toggle_fullscreen_sender = self.toggle_fullscreen_notice.sender();
|
||||
let quit_sender = self.quit_notice.sender();
|
||||
let hide_splash_sender = self.hide_splash_notice.sender();
|
||||
let focus_sender = self.focus_notice.sender();
|
||||
let autoupdater_setup_mutex = self.autoupdater_setup_file.clone();
|
||||
thread::spawn(move || loop {
|
||||
if let Some(msg) = web_rx
|
||||
.recv()
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str::<RPCRequest>(&s).ok())
|
||||
{
|
||||
match msg.get_method() {
|
||||
// The handshake. Here we send some useful data to the WEB UI
|
||||
None if msg.is_handshake() => {
|
||||
web_tx_web.send(RPCResponse::get_handshake()).ok();
|
||||
}
|
||||
Some("win-set-visibility") => toggle_fullscreen_sender.notice(),
|
||||
Some("quit") => quit_sender.notice(),
|
||||
Some("app-ready") => {
|
||||
hide_splash_sender.notice();
|
||||
web_tx_web
|
||||
.send(RPCResponse::visibility_change(true, 1, false))
|
||||
.ok();
|
||||
let command_ref = command_clone.clone();
|
||||
if !command_ref.is_empty() {
|
||||
web_tx_web.send(RPCResponse::open_media(command_ref)).ok();
|
||||
}
|
||||
}
|
||||
Some("app-error") => {
|
||||
hide_splash_sender.notice();
|
||||
if let Some(arg) = msg.get_params() {
|
||||
// TODO: Make this modal dialog
|
||||
eprintln!("Web App Error: {}", arg);
|
||||
}
|
||||
}
|
||||
Some("open-external") => {
|
||||
if let Some(arg) = msg.get_params() {
|
||||
// FIXME: THIS IS NOT SAFE BY ANY MEANS
|
||||
// open::that("calc").ok(); does exactly that
|
||||
let arg = arg.as_str().unwrap_or("");
|
||||
let arg_lc = arg.to_lowercase();
|
||||
if arg_lc.starts_with("http://")
|
||||
|| arg_lc.starts_with("https://")
|
||||
|| arg_lc.starts_with("rtp://")
|
||||
|| arg_lc.starts_with("rtps://")
|
||||
|| arg_lc.starts_with("ftp://")
|
||||
|| arg_lc.starts_with("ipfs://")
|
||||
{
|
||||
open::that(arg).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some("win-focus") => {
|
||||
focus_sender.notice();
|
||||
}
|
||||
Some("autoupdater-notif-clicked") => {
|
||||
// We've shown the "Update Available" notification
|
||||
// and the user clicked on "Restart And Update"
|
||||
let autoupdater_setup_file =
|
||||
autoupdater_setup_mutex.lock().unwrap().clone();
|
||||
match autoupdater_setup_file {
|
||||
Some(file_path) => {
|
||||
println!("Running the setup at {:?}", file_path);
|
||||
|
||||
let command = Command::new(file_path)
|
||||
.args([
|
||||
"/SILENT",
|
||||
"/NOCANCEL",
|
||||
"/FORCECLOSEAPPLICATIONS",
|
||||
"/TASKS=runapp",
|
||||
])
|
||||
.stdout(process::Stdio::null())
|
||||
.stderr(process::Stdio::null())
|
||||
.spawn();
|
||||
|
||||
match command {
|
||||
Ok(process) => {
|
||||
println!("Updater started. (PID {:?})", process.id());
|
||||
quit_sender.notice();
|
||||
}
|
||||
Err(err) => eprintln!("Updater couldn't be started: {err}"),
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
println!("Cannot obtain the setup file path");
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(player_command) if player_command.starts_with("mpv-") => {
|
||||
let resp_json = serde_json::to_string(
|
||||
&msg.args.expect("Cannot have method without args"),
|
||||
)
|
||||
.expect("Cannot build response");
|
||||
player_tx.send(resp_json).ok();
|
||||
}
|
||||
Some(unknown) => {
|
||||
eprintln!("Unsupported command {}({:?})", unknown, msg.get_params())
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
} // recv
|
||||
}); // thread
|
||||
}
|
||||
fn on_min_max(&self, data: &nwg::EventData) {
|
||||
let data = data.on_min_max();
|
||||
data.set_min_size(WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT);
|
||||
}
|
||||
fn on_paint(&self) {
|
||||
if self.splash_screen.visible() {
|
||||
self.splash_screen.resize(self.window.size());
|
||||
} else {
|
||||
self.webview.fit_to_window(self.window.handle.hwnd());
|
||||
}
|
||||
}
|
||||
fn on_toggle_fullscreen_notice(&self) {
|
||||
if let Some(hwnd) = self.window.handle.hwnd() {
|
||||
if let Ok(mut saved_style) = self.saved_window_style.try_borrow_mut() {
|
||||
saved_style.toggle_full_screen(hwnd);
|
||||
self.tray.tray_topmost.set_enabled(!saved_style.full_screen);
|
||||
self.tray
|
||||
.tray_topmost
|
||||
.set_checked((saved_style.ex_style as u32 & WS_EX_TOPMOST) == WS_EX_TOPMOST);
|
||||
self.transmit_window_full_screen_change(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn on_hide_splash_notice(&self) {
|
||||
self.splash_screen.hide();
|
||||
}
|
||||
fn on_focus_notice(&self) {
|
||||
self.window.set_visible(true);
|
||||
if let Some(hwnd) = self.window.handle.hwnd() {
|
||||
if let Ok(mut saved_style) = self.saved_window_style.try_borrow_mut() {
|
||||
saved_style.set_active(hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn on_toggle_topmost(&self) {
|
||||
if let Some(hwnd) = self.window.handle.hwnd() {
|
||||
if let Ok(mut saved_style) = self.saved_window_style.try_borrow_mut() {
|
||||
saved_style.toggle_topmost(hwnd);
|
||||
self.tray
|
||||
.tray_topmost
|
||||
.set_checked((saved_style.ex_style as u32 & WS_EX_TOPMOST) == WS_EX_TOPMOST);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn on_show_hide(&self) {
|
||||
self.window.set_visible(!self.window.visible());
|
||||
self.tray.tray_show_hide.set_checked(self.window.visible());
|
||||
self.transmit_window_state_change();
|
||||
}
|
||||
fn on_quit(&self, data: &nwg::EventData) {
|
||||
if let nwg::EventData::OnWindowClose(data) = data {
|
||||
data.close(false);
|
||||
}
|
||||
self.window.set_visible(false);
|
||||
self.tray.tray_show_hide.set_checked(self.window.visible());
|
||||
self.transmit_window_full_screen_change(false);
|
||||
// Terminates the app regardless if the user is set exit on window closed or not
|
||||
// nwg::stop_thread_dispatch();
|
||||
}
|
||||
}
|
||||
13
src/stremio_app/constants.rs
Normal file
13
src/stremio_app/constants.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
pub const APP_NAME: &str = "Stremio";
|
||||
pub const IPC_PATH: &str = "//./pipe/com.stremio5.";
|
||||
pub const DEV_ENDPOINT: &str = "http://127.0.0.1:11470";
|
||||
pub const WEB_ENDPOINT: &str = "https://app.strem.io/shell-v4.4/";
|
||||
pub const STA_ENDPOINT: &str = "https://staging.strem.io/";
|
||||
pub const WINDOW_MIN_WIDTH: i32 = 1000;
|
||||
pub const WINDOW_MIN_HEIGHT: i32 = 600;
|
||||
pub const UPDATE_INTERVAL: u64 = 12 * 60 * 60;
|
||||
pub const UPDATE_ENDPOINT: [&str; 3] = [
|
||||
"https://www.strem.io/updater/check?product=stremio-shell-ng",
|
||||
"https://www.stremio.com/updater/check?product=stremio-shell-ng",
|
||||
"https://www.stremio.net/updater/check?product=stremio-shell-ng",
|
||||
];
|
||||
108
src/stremio_app/ipc.rs
Normal file
108
src/stremio_app/ipc.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{self, json};
|
||||
use std::cell::RefCell;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub type Channel = RefCell<Option<(flume::Sender<String>, flume::Receiver<String>)>>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct RPCRequest {
|
||||
pub id: u64,
|
||||
pub args: Option<Vec<serde_json::Value>>,
|
||||
}
|
||||
|
||||
impl RPCRequest {
|
||||
pub fn is_handshake(&self) -> bool {
|
||||
self.id == 0
|
||||
}
|
||||
pub fn get_method(&self) -> Option<&str> {
|
||||
self.args
|
||||
.as_ref()
|
||||
.and_then(|args| args.first())
|
||||
.and_then(|arg| arg.as_str())
|
||||
}
|
||||
pub fn get_params(&self) -> Option<&serde_json::Value> {
|
||||
self.args
|
||||
.as_ref()
|
||||
.and_then(|args| if args.len() > 1 { Some(&args[1]) } else { None })
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct RPCResponseDataTransport {
|
||||
pub properties: Vec<Vec<String>>,
|
||||
pub signals: Vec<String>,
|
||||
pub methods: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct RPCResponseData {
|
||||
pub transport: RPCResponseDataTransport,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct RPCResponse {
|
||||
pub id: u64,
|
||||
pub object: String,
|
||||
#[serde(rename = "type")]
|
||||
pub response_type: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<RPCResponseData>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub args: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl RPCResponse {
|
||||
pub fn get_handshake() -> String {
|
||||
let resp = RPCResponse {
|
||||
id: 0,
|
||||
object: "transport".to_string(),
|
||||
response_type: 3,
|
||||
data: Some(RPCResponseData {
|
||||
transport: RPCResponseDataTransport {
|
||||
properties: vec![
|
||||
vec![],
|
||||
vec![
|
||||
"".to_string(),
|
||||
"shellVersion".to_string(),
|
||||
"".to_string(),
|
||||
VERSION.to_string(),
|
||||
],
|
||||
],
|
||||
signals: vec![],
|
||||
methods: vec![vec!["onEvent".to_string(), "".to_string()]],
|
||||
},
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
serde_json::to_string(&resp).expect("Cannot build response")
|
||||
}
|
||||
pub fn response_message(msg: Option<serde_json::Value>) -> String {
|
||||
let resp = RPCResponse {
|
||||
id: 1,
|
||||
object: "transport".to_string(),
|
||||
response_type: 1,
|
||||
args: msg,
|
||||
..Default::default()
|
||||
};
|
||||
serde_json::to_string(&resp).expect("Cannot build response")
|
||||
}
|
||||
pub fn visibility_change(visible: bool, visibility: u32, is_full_screen: bool) -> String {
|
||||
Self::response_message(Some(json!(["win-visibility-changed" ,{
|
||||
"visible": visible,
|
||||
"visibility": visibility,
|
||||
"isFullscreen": is_full_screen
|
||||
}])))
|
||||
}
|
||||
pub fn state_change(state: u32) -> String {
|
||||
Self::response_message(Some(json!(["win-state-changed" ,{
|
||||
"state": state,
|
||||
}])))
|
||||
}
|
||||
pub fn open_media(url: String) -> String {
|
||||
Self::response_message(Some(json!(["open-media", url])))
|
||||
}
|
||||
pub fn update_available() -> String {
|
||||
Self::response_message(Some(json!(["autoupdater-show-notif"])))
|
||||
}
|
||||
}
|
||||
14
src/stremio_app/mod.rs
Normal file
14
src/stremio_app/mod.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
pub mod app;
|
||||
pub use app::MainWindow;
|
||||
pub mod ipc;
|
||||
pub mod stremio_player;
|
||||
pub mod stremio_server;
|
||||
pub mod stremio_wevbiew;
|
||||
pub use ipc::RPCResponse;
|
||||
pub mod named_pipe;
|
||||
pub mod splash;
|
||||
pub mod systray;
|
||||
pub mod window_helper;
|
||||
pub use named_pipe::{PipeClient, PipeServer};
|
||||
pub mod constants;
|
||||
pub mod updater;
|
||||
252
src/stremio_app/named_pipe.rs
Normal file
252
src/stremio_app/named_pipe.rs
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
// Based on
|
||||
// https://gitlab.com/tbsaunde/windows-named-pipe/-/blob/f4fd29191f0541f85f818885275dc4573d4059ec/src/lib.rs
|
||||
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::io::{self, Read, Write};
|
||||
use std::os::windows::prelude::OsStrExt;
|
||||
use std::path::Path;
|
||||
use winapi::shared::minwindef::{DWORD, LPCVOID, LPVOID};
|
||||
use winapi::shared::winerror;
|
||||
use winapi::um::fileapi::OPEN_EXISTING;
|
||||
use winapi::um::fileapi::{CreateFileW, FlushFileBuffers, ReadFile, WriteFile};
|
||||
use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE};
|
||||
use winapi::um::namedpipeapi::{
|
||||
ConnectNamedPipe, CreateNamedPipeW, DisconnectNamedPipe, WaitNamedPipeW,
|
||||
};
|
||||
use winapi::um::winbase::{
|
||||
FILE_FLAG_FIRST_PIPE_INSTANCE, PIPE_ACCESS_DUPLEX, PIPE_READMODE_BYTE, PIPE_TYPE_BYTE,
|
||||
PIPE_UNLIMITED_INSTANCES, PIPE_WAIT,
|
||||
};
|
||||
use winapi::um::winnt::{FILE_ATTRIBUTE_NORMAL, GENERIC_READ, GENERIC_WRITE, HANDLE};
|
||||
#[derive(Debug)]
|
||||
pub struct PipeClient {
|
||||
is_server: bool,
|
||||
handle: Handle,
|
||||
}
|
||||
|
||||
impl PipeClient {
|
||||
fn create_pipe(path: &Path) -> io::Result<HANDLE> {
|
||||
let mut os_str: OsString = path.as_os_str().into();
|
||||
os_str.push("\x00");
|
||||
let u16_slice = os_str.encode_wide().collect::<Vec<u16>>();
|
||||
|
||||
unsafe { WaitNamedPipeW(u16_slice.as_ptr(), 0) };
|
||||
let handle: *mut winapi::ctypes::c_void = unsafe {
|
||||
CreateFileW(
|
||||
u16_slice.as_ptr(),
|
||||
GENERIC_READ | GENERIC_WRITE,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
if handle != INVALID_HANDLE_VALUE {
|
||||
Ok(handle)
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect<P: AsRef<Path>>(path: P) -> io::Result<PipeClient> {
|
||||
let handle = PipeClient::create_pipe(path.as_ref())?;
|
||||
|
||||
Ok(PipeClient {
|
||||
handle: Handle { inner: handle },
|
||||
is_server: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PipeClient {
|
||||
fn drop(&mut self) {
|
||||
unsafe { FlushFileBuffers(self.handle.inner) };
|
||||
if self.is_server {
|
||||
unsafe { DisconnectNamedPipe(self.handle.inner) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for PipeClient {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let mut bytes_read = 0;
|
||||
let ok = unsafe {
|
||||
ReadFile(
|
||||
self.handle.inner,
|
||||
buf.as_mut_ptr() as LPVOID,
|
||||
buf.len() as DWORD,
|
||||
&mut bytes_read,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
if ok != 0 {
|
||||
Ok(bytes_read as usize)
|
||||
} else {
|
||||
match io::Error::last_os_error().raw_os_error().map(|x| x as u32) {
|
||||
Some(winerror::ERROR_PIPE_NOT_CONNECTED) => Ok(0),
|
||||
Some(err) => Err(io::Error::from_raw_os_error(err as i32)),
|
||||
_ => panic!(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for PipeClient {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let mut bytes_written = 0;
|
||||
let ok = unsafe {
|
||||
WriteFile(
|
||||
self.handle.inner,
|
||||
buf.as_ptr() as LPCVOID,
|
||||
buf.len() as DWORD,
|
||||
&mut bytes_written,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
if ok != 0 {
|
||||
Ok(bytes_written as usize)
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
let ok = unsafe { FlushFileBuffers(self.handle.inner) };
|
||||
|
||||
if ok != 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PipeServer {
|
||||
path: Vec<u16>,
|
||||
next_pipe: Handle,
|
||||
}
|
||||
|
||||
fn to_u16s<S: AsRef<OsStr>>(s: S) -> io::Result<Vec<u16>> {
|
||||
let mut maybe_result: Vec<u16> = s.as_ref().encode_wide().collect();
|
||||
if maybe_result.iter().any(|&u| u == 0) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"strings passed to WinAPI cannot contain NULs",
|
||||
));
|
||||
}
|
||||
maybe_result.push(0);
|
||||
Ok(maybe_result)
|
||||
}
|
||||
|
||||
impl PipeServer {
|
||||
fn create_pipe(path: &[u16], first: bool) -> io::Result<Handle> {
|
||||
let mut access_flags = PIPE_ACCESS_DUPLEX;
|
||||
if first {
|
||||
access_flags |= FILE_FLAG_FIRST_PIPE_INSTANCE;
|
||||
}
|
||||
let handle = unsafe {
|
||||
CreateNamedPipeW(
|
||||
path.as_ptr(),
|
||||
access_flags,
|
||||
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
|
||||
PIPE_UNLIMITED_INSTANCES,
|
||||
65536,
|
||||
65536,
|
||||
50,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
if handle != INVALID_HANDLE_VALUE {
|
||||
Ok(Handle { inner: handle })
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
|
||||
fn connect_pipe(handle: &Handle) -> io::Result<()> {
|
||||
let result = unsafe { ConnectNamedPipe(handle.inner, std::ptr::null_mut()) };
|
||||
|
||||
if result != 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
match io::Error::last_os_error().raw_os_error().map(|x| x as u32) {
|
||||
Some(winerror::ERROR_PIPE_CONNECTED) => Ok(()),
|
||||
Some(err) => Err(io::Error::from_raw_os_error(err as i32)),
|
||||
_ => panic!(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind<P: AsRef<Path>>(path: P) -> io::Result<Self> {
|
||||
let path = to_u16s(path.as_ref().as_os_str())?;
|
||||
let next_pipe = PipeServer::create_pipe(&path, true)?;
|
||||
Ok(PipeServer { path, next_pipe })
|
||||
}
|
||||
|
||||
pub fn accept(&mut self) -> io::Result<PipeClient> {
|
||||
let handle = std::mem::replace(
|
||||
&mut self.next_pipe,
|
||||
PipeServer::create_pipe(&self.path, false)?,
|
||||
);
|
||||
|
||||
PipeServer::connect_pipe(&handle)?;
|
||||
|
||||
Ok(PipeClient {
|
||||
handle,
|
||||
is_server: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Handle {
|
||||
inner: HANDLE,
|
||||
}
|
||||
|
||||
impl Drop for Handle {
|
||||
fn drop(&mut self) {
|
||||
unsafe { CloseHandle(self.inner) };
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Sync for Handle {}
|
||||
unsafe impl Send for Handle {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::thread;
|
||||
|
||||
#[test]
|
||||
fn duplex_communication() {
|
||||
let socket_path = Path::new("//./pipe/basicsock");
|
||||
println!("{:?}", socket_path);
|
||||
let msg1 = b"hello";
|
||||
let msg2 = b"world!";
|
||||
|
||||
let mut listener = PipeServer::bind(socket_path).unwrap();
|
||||
let thread = thread::spawn(move || {
|
||||
let mut stream = listener.accept().unwrap();
|
||||
let mut buf = [0; 5];
|
||||
stream.read(&mut buf).unwrap();
|
||||
assert_eq!(&msg1[..], &buf[..]);
|
||||
stream.write_all(msg2).unwrap();
|
||||
});
|
||||
|
||||
let mut stream = PipeClient::connect(socket_path).unwrap();
|
||||
|
||||
stream.write_all(msg1).unwrap();
|
||||
let mut buf = vec![];
|
||||
stream.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(&msg2[..], &buf[..]);
|
||||
drop(stream);
|
||||
|
||||
thread.join().unwrap();
|
||||
}
|
||||
}
|
||||
32
src/stremio_app/splash.rs
Normal file
32
src/stremio_app/splash.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use native_windows_derive::NwgPartial;
|
||||
use native_windows_gui as nwg;
|
||||
use std::cmp;
|
||||
|
||||
#[derive(Default, NwgPartial)]
|
||||
pub struct SplashImage {
|
||||
#[nwg_resource]
|
||||
embed: nwg::EmbedResource,
|
||||
#[nwg_resource(size: Some((300,300)), source_embed: Some(&data.embed), source_embed_str: Some("SPLASHIMAGE"))]
|
||||
splash_image: nwg::Bitmap,
|
||||
#[nwg_control(background_color: Some(Self::BG_COLOR))]
|
||||
splash_frame: nwg::ImageFrame,
|
||||
#[nwg_control(parent: splash_frame, background_color: Some(Self::BG_COLOR), bitmap: Some(&data.splash_image))]
|
||||
splash: nwg::ImageFrame,
|
||||
}
|
||||
|
||||
impl SplashImage {
|
||||
const BG_COLOR: [u8; 3] = [27, 17, 38];
|
||||
pub fn resize(&self, size: (u32, u32)) {
|
||||
let (w, h) = size;
|
||||
let s = cmp::min(w, h);
|
||||
self.splash_frame.set_size(w, h);
|
||||
self.splash.set_size(s, s);
|
||||
self.splash.set_position(w as i32 / 2 - s as i32 / 2, 0);
|
||||
}
|
||||
pub fn visible(&self) -> bool {
|
||||
self.splash_frame.visible()
|
||||
}
|
||||
pub fn hide(&self) {
|
||||
self.splash_frame.set_visible(false);
|
||||
}
|
||||
}
|
||||
253
src/stremio_app/stremio_player/communication.rs
Normal file
253
src/stremio_app/stremio_player/communication.rs
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
use core::convert::TryFrom;
|
||||
use libmpv::{events::PropertyData, mpv_end_file_reason, EndFileReason};
|
||||
use parse_display::{Display, FromStr};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
// Responses
|
||||
const JSON_RESPONSES: [&str; 3] = ["track-list", "video-params", "metadata"];
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
pub struct PlayerProprChange {
|
||||
name: String,
|
||||
data: serde_json::Value,
|
||||
}
|
||||
impl PlayerProprChange {
|
||||
fn value_from_format(data: PropertyData, as_json: bool) -> serde_json::Value {
|
||||
match data {
|
||||
PropertyData::Flag(d) => serde_json::Value::Bool(d),
|
||||
PropertyData::Int64(d) => serde_json::Value::Number(
|
||||
serde_json::Number::from_f64(d as f64).expect("MPV returned invalid number"),
|
||||
),
|
||||
PropertyData::Double(d) => serde_json::Value::Number(
|
||||
serde_json::Number::from_f64(d).expect("MPV returned invalid number"),
|
||||
),
|
||||
PropertyData::OsdStr(s) => serde_json::Value::String(s.to_string()),
|
||||
PropertyData::Str(s) => {
|
||||
if as_json {
|
||||
serde_json::from_str(s).expect("MPV returned invalid JSON data")
|
||||
} else {
|
||||
serde_json::Value::String(s.to_string())
|
||||
}
|
||||
}
|
||||
PropertyData::Node(_) => unimplemented!("`PropertyData::Node` is not supported"),
|
||||
}
|
||||
}
|
||||
pub fn from_name_value(name: String, value: PropertyData) -> Self {
|
||||
let is_json = JSON_RESPONSES.contains(&name.as_str());
|
||||
Self {
|
||||
name,
|
||||
data: Self::value_from_format(value, is_json),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
pub struct PlayerEnded {
|
||||
reason: String,
|
||||
}
|
||||
impl PlayerEnded {
|
||||
fn string_from_end_reason(data: EndFileReason) -> String {
|
||||
match data {
|
||||
mpv_end_file_reason::Error => "error".to_string(),
|
||||
mpv_end_file_reason::Quit => "quit".to_string(),
|
||||
_ => "other".to_string(),
|
||||
}
|
||||
}
|
||||
pub fn from_end_reason(data: EndFileReason) -> Self {
|
||||
Self {
|
||||
reason: Self::string_from_end_reason(data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct PlayerError {
|
||||
pub error: String,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum PlayerEvent {
|
||||
PropChange(PlayerProprChange),
|
||||
End(PlayerEnded),
|
||||
Error(PlayerError),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct PlayerResponse<'a>(pub &'a str, pub PlayerEvent);
|
||||
impl PlayerResponse<'_> {
|
||||
pub fn to_value(&self) -> Option<serde_json::Value> {
|
||||
serde_json::to_value(self).ok()
|
||||
}
|
||||
}
|
||||
|
||||
// Player incoming messages from the web UI
|
||||
/*
|
||||
Message general case - ["function-name", ["arguments", ...]]
|
||||
The function could be either mpv-observe-prop, mpv-set-prop or mpv-command.
|
||||
|
||||
["mpv-observe-prop", "prop-name"]
|
||||
["mpv-set-prop", ["prop-name", prop-val]]
|
||||
["mpv-command", ["command-name"<, "arguments">]]
|
||||
|
||||
All the function and property names are in kebab-case.
|
||||
|
||||
MPV requires type for any prop-name when observing or setting it's value.
|
||||
The type for setting is not always the same as the type for observing the prop.
|
||||
|
||||
"mpv-observe-prop" function is the only one that accepts single string
|
||||
instead of array of arguments
|
||||
|
||||
"mpv-command" function always takes an array even if the command doesn't
|
||||
have any arguments. For example this are the commands we support:
|
||||
|
||||
["mpv-command", ["loadfile", "file name"]]
|
||||
["mpv-command", ["stop"]]
|
||||
*/
|
||||
macro_rules! stringable {
|
||||
($t:ident) => {
|
||||
impl From<$t> for String {
|
||||
fn from(s: $t) -> Self {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
impl TryFrom<String> for $t {
|
||||
type Error = parse_display::ParseError;
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
s.parse()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
pub enum InMsgFn {
|
||||
MpvSetProp,
|
||||
MpvCommand,
|
||||
MpvObserveProp,
|
||||
}
|
||||
stringable!(InMsgFn);
|
||||
// Bool
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
pub enum BoolProp {
|
||||
Pause,
|
||||
PausedForCache,
|
||||
Seeking,
|
||||
EofReached,
|
||||
}
|
||||
stringable!(BoolProp);
|
||||
// Int
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
pub enum IntProp {
|
||||
Aid,
|
||||
Vid,
|
||||
Sid,
|
||||
}
|
||||
stringable!(IntProp);
|
||||
// Fp
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
pub enum FpProp {
|
||||
TimePos,
|
||||
Mute,
|
||||
Volume,
|
||||
Duration,
|
||||
SubScale,
|
||||
CacheBufferingState,
|
||||
SubPos,
|
||||
Speed,
|
||||
}
|
||||
stringable!(FpProp);
|
||||
// Str
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
pub enum StrProp {
|
||||
FfmpegVersion,
|
||||
Hwdec,
|
||||
InputDefaltBindings,
|
||||
InputVoKeyboard,
|
||||
Metadata,
|
||||
MpvVersion,
|
||||
Osc,
|
||||
Path,
|
||||
SubAssOverride,
|
||||
SubBackColor,
|
||||
SubBorderColor,
|
||||
SubColor,
|
||||
TrackList,
|
||||
VideoParams,
|
||||
// Vo,
|
||||
}
|
||||
stringable!(StrProp);
|
||||
|
||||
// Any
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum PropKey {
|
||||
Bool(BoolProp),
|
||||
Int(IntProp),
|
||||
Fp(FpProp),
|
||||
Str(StrProp),
|
||||
}
|
||||
impl fmt::Display for PropKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Bool(v) => write!(f, "{}", v),
|
||||
Self::Int(v) => write!(f, "{}", v),
|
||||
Self::Fp(v) => write!(f, "{}", v),
|
||||
Self::Str(v) => write!(f, "{}", v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum PropVal {
|
||||
Bool(bool),
|
||||
Str(String),
|
||||
Num(f64),
|
||||
}
|
||||
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
#[serde(untagged)]
|
||||
pub enum MpvCmd {
|
||||
Loadfile,
|
||||
Stop,
|
||||
}
|
||||
stringable!(MpvCmd);
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum CmdVal {
|
||||
Single((MpvCmd,)),
|
||||
Double(MpvCmd, String),
|
||||
}
|
||||
impl From<CmdVal> for Vec<String> {
|
||||
fn from(cmd: CmdVal) -> Vec<String> {
|
||||
match cmd {
|
||||
CmdVal::Single(cmd) => vec![cmd.0.to_string()],
|
||||
CmdVal::Double(cmd, arg) => vec![cmd.to_string(), arg],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum InMsgArgs {
|
||||
StProp(PropKey, PropVal),
|
||||
Cmd(CmdVal),
|
||||
ObProp(PropKey),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct InMsg(pub InMsgFn, pub InMsgArgs);
|
||||
167
src/stremio_app/stremio_player/communication_tests.rs
Normal file
167
src/stremio_app/stremio_player/communication_tests.rs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
use crate::stremio_app::stremio_player::communication::{
|
||||
BoolProp, CmdVal, InMsg, InMsgArgs, InMsgFn, MpvCmd, PlayerEnded, PlayerProprChange, PropKey,
|
||||
PropVal,
|
||||
};
|
||||
use libmpv::{events::PropertyData, mpv_end_file_reason};
|
||||
|
||||
use serde_test::{assert_tokens, Token};
|
||||
|
||||
#[test]
|
||||
fn propr_change_tokens() {
|
||||
let prop = "test-prop";
|
||||
let tokens: [Token; 6] = [
|
||||
Token::Struct {
|
||||
name: "PlayerProprChange",
|
||||
len: 2,
|
||||
},
|
||||
Token::Str("name"),
|
||||
Token::None,
|
||||
Token::Str("data"),
|
||||
Token::None,
|
||||
Token::StructEnd,
|
||||
];
|
||||
|
||||
fn tokens_by_type(tokens: &[Token; 6], name: &'static str, val: PropertyData, token: Token) {
|
||||
let mut typed_tokens = tokens.clone();
|
||||
typed_tokens[2] = Token::Str(name);
|
||||
typed_tokens[4] = token;
|
||||
assert_tokens(
|
||||
&PlayerProprChange::from_name_value(name.to_string(), val),
|
||||
&typed_tokens,
|
||||
);
|
||||
}
|
||||
tokens_by_type(&tokens, prop, PropertyData::Flag(true), Token::Bool(true));
|
||||
tokens_by_type(&tokens, prop, PropertyData::Int64(1), Token::F64(1.0));
|
||||
tokens_by_type(&tokens, prop, PropertyData::Double(1.0), Token::F64(1.0));
|
||||
tokens_by_type(&tokens, prop, PropertyData::OsdStr("ok"), Token::Str("ok"));
|
||||
tokens_by_type(&tokens, prop, PropertyData::Str("ok"), Token::Str("ok"));
|
||||
|
||||
// JSON response
|
||||
tokens_by_type(
|
||||
&tokens,
|
||||
"track-list",
|
||||
PropertyData::Str(r#""ok""#),
|
||||
Token::Str("ok"),
|
||||
);
|
||||
tokens_by_type(
|
||||
&tokens,
|
||||
"video-params",
|
||||
PropertyData::Str(r#""ok""#),
|
||||
Token::Str("ok"),
|
||||
);
|
||||
tokens_by_type(
|
||||
&tokens,
|
||||
"metadata",
|
||||
PropertyData::Str(r#""ok""#),
|
||||
Token::Str("ok"),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ended_tokens() {
|
||||
let tokens: [Token; 4] = [
|
||||
Token::Struct {
|
||||
name: "PlayerEnded",
|
||||
len: 1,
|
||||
},
|
||||
Token::Str("reason"),
|
||||
Token::None,
|
||||
Token::StructEnd,
|
||||
];
|
||||
let mut typed_tokens = tokens.clone();
|
||||
typed_tokens[2] = Token::Str("error");
|
||||
assert_tokens(
|
||||
&PlayerEnded::from_end_reason(mpv_end_file_reason::Error),
|
||||
&typed_tokens,
|
||||
);
|
||||
let mut typed_tokens = tokens.clone();
|
||||
typed_tokens[2] = Token::Str("quit");
|
||||
assert_tokens(
|
||||
&PlayerEnded::from_end_reason(mpv_end_file_reason::Quit),
|
||||
&typed_tokens,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ob_propr_tokens() {
|
||||
assert_tokens(
|
||||
&InMsg(
|
||||
InMsgFn::MpvObserveProp,
|
||||
InMsgArgs::ObProp(PropKey::Bool(BoolProp::Pause)),
|
||||
),
|
||||
&[
|
||||
Token::TupleStruct {
|
||||
name: "InMsg",
|
||||
len: 2,
|
||||
},
|
||||
Token::Str("mpv-observe-prop"),
|
||||
Token::Str("pause"),
|
||||
Token::TupleStructEnd,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_propr_tokens() {
|
||||
assert_tokens(
|
||||
&InMsg(
|
||||
InMsgFn::MpvSetProp,
|
||||
InMsgArgs::StProp(PropKey::Bool(BoolProp::Pause), PropVal::Bool(true)),
|
||||
),
|
||||
&[
|
||||
Token::TupleStruct {
|
||||
name: "InMsg",
|
||||
len: 2,
|
||||
},
|
||||
Token::Str("mpv-set-prop"),
|
||||
Token::Tuple { len: 2 },
|
||||
Token::Str("pause"),
|
||||
Token::Bool(true),
|
||||
Token::TupleEnd,
|
||||
Token::TupleStructEnd,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_stop_tokens() {
|
||||
assert_tokens(
|
||||
&InMsg(
|
||||
InMsgFn::MpvCommand,
|
||||
InMsgArgs::Cmd(CmdVal::Single((MpvCmd::Stop,))),
|
||||
),
|
||||
&[
|
||||
Token::TupleStruct {
|
||||
name: "InMsg",
|
||||
len: 2,
|
||||
},
|
||||
Token::Str("mpv-command"),
|
||||
Token::Tuple { len: 1 },
|
||||
Token::Str("stop"),
|
||||
Token::TupleEnd,
|
||||
Token::TupleStructEnd,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_loadfile_tokens() {
|
||||
assert_tokens(
|
||||
&InMsg(
|
||||
InMsgFn::MpvCommand,
|
||||
InMsgArgs::Cmd(CmdVal::Double(MpvCmd::Loadfile, "some_file".to_string())),
|
||||
),
|
||||
&[
|
||||
Token::TupleStruct {
|
||||
name: "InMsg",
|
||||
len: 2,
|
||||
},
|
||||
Token::Str("mpv-command"),
|
||||
Token::Tuple { len: 2 },
|
||||
Token::Str("loadfile"),
|
||||
Token::Str("some_file"),
|
||||
Token::TupleEnd,
|
||||
Token::TupleStructEnd,
|
||||
],
|
||||
);
|
||||
}
|
||||
9
src/stremio_app/stremio_player/mod.rs
Normal file
9
src/stremio_app/stremio_player/mod.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
pub mod player;
|
||||
pub use player::Player;
|
||||
pub mod communication;
|
||||
pub use communication::{
|
||||
CmdVal, InMsg, InMsgArgs, InMsgFn, PlayerEnded, PlayerEvent, PlayerProprChange, PlayerResponse,
|
||||
PropKey, PropVal,
|
||||
};
|
||||
#[cfg(test)]
|
||||
mod communication_tests;
|
||||
231
src/stremio_app/stremio_player/player.rs
Normal file
231
src/stremio_app/stremio_player/player.rs
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
use crate::stremio_app::ipc;
|
||||
use crate::stremio_app::RPCResponse;
|
||||
use flume::{Receiver, Sender};
|
||||
use libmpv::{events::Event, Format, Mpv, SetData};
|
||||
use native_windows_gui::{self as nwg, PartialUi};
|
||||
use std::{
|
||||
sync::Arc,
|
||||
thread::{self, JoinHandle},
|
||||
};
|
||||
use winapi::shared::windef::HWND;
|
||||
|
||||
use crate::stremio_app::stremio_player::{
|
||||
CmdVal, InMsg, InMsgArgs, InMsgFn, PlayerEnded, PlayerEvent, PlayerProprChange, PlayerResponse,
|
||||
PropKey, PropVal,
|
||||
};
|
||||
|
||||
struct ObserveProperty {
|
||||
name: String,
|
||||
format: Format,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Player {
|
||||
pub channel: ipc::Channel,
|
||||
}
|
||||
|
||||
impl PartialUi for Player {
|
||||
fn build_partial<W: Into<nwg::ControlHandle>>(
|
||||
// @TODO replace with `&mut self`?
|
||||
data: &mut Self,
|
||||
parent: Option<W>,
|
||||
) -> Result<(), nwg::NwgError> {
|
||||
// @TODO replace all `expect`s with proper error handling?
|
||||
|
||||
let window_handle = parent
|
||||
.expect("no parent window")
|
||||
.into()
|
||||
.hwnd()
|
||||
.expect("cannot obtain window handle");
|
||||
|
||||
let (in_msg_sender, in_msg_receiver) = flume::unbounded();
|
||||
let (rpc_response_sender, rpc_response_receiver) = flume::unbounded();
|
||||
let (observe_property_sender, observe_property_receiver) = flume::unbounded();
|
||||
data.channel = ipc::Channel::new(Some((in_msg_sender, rpc_response_receiver)));
|
||||
|
||||
let mpv = create_shareable_mpv(window_handle);
|
||||
|
||||
let _event_thread = create_event_thread(
|
||||
Arc::clone(&mpv),
|
||||
observe_property_receiver,
|
||||
rpc_response_sender,
|
||||
);
|
||||
let _message_thread = create_message_thread(mpv, observe_property_sender, in_msg_receiver);
|
||||
// @TODO implement a mechanism to stop threads on `Player` drop if needed
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_shareable_mpv(window_handle: HWND) -> Arc<Mpv> {
|
||||
let mpv = Mpv::with_initializer(|initializer| {
|
||||
macro_rules! set_property {
|
||||
($name:literal, $value:expr) => {
|
||||
initializer
|
||||
.set_property($name, $value)
|
||||
.expect(concat!("failed to set ", $name));
|
||||
};
|
||||
}
|
||||
set_property!("wid", window_handle as i64);
|
||||
// initializer.set_property("vo", "gpu").expect("unable to set vo");
|
||||
// win, opengl: works but least performancy, 10-15% CPU
|
||||
// winvk, vulkan: works as good as d3d11
|
||||
// d3d11, d1d11: works great
|
||||
// dxinterop, auto: works, slightly more cpu use than d3d11
|
||||
// default (auto) seems to be d3d11 (vo/gpu/d3d11)
|
||||
set_property!("gpu-context", "angle");
|
||||
set_property!("gpu-api", "auto");
|
||||
set_property!("title", "Stremio");
|
||||
set_property!("terminal", "yes");
|
||||
set_property!("msg-level", "all=no,cplayer=debug");
|
||||
set_property!("quiet", "yes");
|
||||
set_property!("hwdec", "auto");
|
||||
// FIXME: very often the audio track isn't selected when using "aid" = "auto"
|
||||
set_property!("aid", "1");
|
||||
Ok(())
|
||||
});
|
||||
Arc::new(mpv.expect("cannot build MPV"))
|
||||
}
|
||||
|
||||
fn create_event_thread(
|
||||
mpv: Arc<Mpv>,
|
||||
observe_property_receiver: Receiver<ObserveProperty>,
|
||||
rpc_response_sender: Sender<String>,
|
||||
) -> JoinHandle<()> {
|
||||
thread::spawn(move || {
|
||||
let mut event_context = mpv.create_event_context();
|
||||
event_context
|
||||
.disable_deprecated_events()
|
||||
.expect("failed to disable deprecated MPV events");
|
||||
|
||||
// -- Event handler loop --
|
||||
|
||||
loop {
|
||||
for ObserveProperty { name, format } in observe_property_receiver.drain() {
|
||||
event_context
|
||||
.observe_property(&name, format, 0)
|
||||
.expect("failed to observer MPV property");
|
||||
}
|
||||
|
||||
// -1.0 means to block and wait for an event.
|
||||
let event = match event_context.wait_event(-1.) {
|
||||
Some(Ok(event)) => event,
|
||||
Some(Err(error)) => {
|
||||
eprintln!("Event errored: {error:?}");
|
||||
continue;
|
||||
}
|
||||
// dummy event received (may be created on a wake up call or on timeout)
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// even if you don't do anything with the events, it is still necessary to empty the event loop
|
||||
let player_response = match event {
|
||||
Event::PropertyChange { name, change, .. } => PlayerResponse(
|
||||
"mpv-prop-change",
|
||||
PlayerEvent::PropChange(PlayerProprChange::from_name_value(
|
||||
name.to_string(),
|
||||
change,
|
||||
)),
|
||||
),
|
||||
Event::EndFile(reason) => PlayerResponse(
|
||||
"mpv-event-ended",
|
||||
PlayerEvent::End(PlayerEnded::from_end_reason(reason)),
|
||||
),
|
||||
Event::Shutdown => {
|
||||
break;
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
rpc_response_sender
|
||||
.send(RPCResponse::response_message(player_response.to_value()))
|
||||
.expect("failed to send RPCResponse");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn create_message_thread(
|
||||
mpv: Arc<Mpv>,
|
||||
observe_property_sender: Sender<ObserveProperty>,
|
||||
in_msg_receiver: Receiver<String>,
|
||||
) -> JoinHandle<()> {
|
||||
thread::spawn(move || {
|
||||
// -- Helpers --
|
||||
|
||||
let observe_property = |name: String, format: Format| {
|
||||
observe_property_sender
|
||||
.send(ObserveProperty { name, format })
|
||||
.expect("cannot send ObserveProperty");
|
||||
mpv.wake_up();
|
||||
};
|
||||
|
||||
let send_command = |cmd: CmdVal| {
|
||||
let (name, arg) = match cmd {
|
||||
CmdVal::Double(name, arg) => (name, format!(r#""{arg}""#)),
|
||||
CmdVal::Single((name,)) => (name, String::new()),
|
||||
};
|
||||
if let Err(error) = mpv.command(&name.to_string(), &[&arg]) {
|
||||
eprintln!("failed to execute MPV command: '{error:#}'")
|
||||
}
|
||||
};
|
||||
|
||||
fn set_property(name: impl ToString, value: impl SetData, mpv: &Mpv) {
|
||||
if let Err(error) = mpv.set_property(&name.to_string(), value) {
|
||||
eprintln!("cannot set MPV property: '{error:#}'")
|
||||
}
|
||||
}
|
||||
|
||||
// -- InMsg handler loop --
|
||||
|
||||
for msg in in_msg_receiver.iter() {
|
||||
let in_msg: InMsg = match serde_json::from_str(&msg) {
|
||||
Ok(in_msg) => in_msg,
|
||||
Err(error) => {
|
||||
eprintln!("cannot parse InMsg: {error:#}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match in_msg {
|
||||
InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Bool(prop))) => {
|
||||
observe_property(prop.to_string(), Format::Flag);
|
||||
}
|
||||
InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Int(prop))) => {
|
||||
observe_property(prop.to_string(), Format::Int64);
|
||||
}
|
||||
InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Fp(prop))) => {
|
||||
observe_property(prop.to_string(), Format::Double);
|
||||
}
|
||||
InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Str(prop))) => {
|
||||
observe_property(prop.to_string(), Format::String);
|
||||
}
|
||||
InMsg(InMsgFn::MpvSetProp, InMsgArgs::StProp(name, PropVal::Bool(value))) => {
|
||||
set_property(name, value, &mpv);
|
||||
}
|
||||
InMsg(InMsgFn::MpvSetProp, InMsgArgs::StProp(name, PropVal::Num(value))) => {
|
||||
set_property(name, value, &mpv);
|
||||
}
|
||||
InMsg(InMsgFn::MpvSetProp, InMsgArgs::StProp(name, PropVal::Str(value))) => {
|
||||
set_property(name, value, &mpv);
|
||||
}
|
||||
InMsg(InMsgFn::MpvCommand, InMsgArgs::Cmd(cmd)) => {
|
||||
send_command(cmd);
|
||||
}
|
||||
msg => {
|
||||
eprintln!("MPV unsupported message: '{msg:?}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
trait MpvExt {
|
||||
fn wake_up(&self);
|
||||
}
|
||||
|
||||
impl MpvExt for Mpv {
|
||||
// @TODO create a PR to the `libmpv` crate and then remove `libmpv-sys` from Cargo.toml?
|
||||
fn wake_up(&self) {
|
||||
unsafe { libmpv_sys::mpv_wakeup(self.ctx.as_ptr()) }
|
||||
}
|
||||
}
|
||||
2
src/stremio_app/stremio_server/mod.rs
Normal file
2
src/stremio_app/stremio_server/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod server;
|
||||
pub use server::StremioServer;
|
||||
45
src/stremio_app/stremio_server/server.rs
Normal file
45
src/stremio_app/stremio_server/server.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use native_windows_gui as nwg;
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::process::Command;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use win32job::Job;
|
||||
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
pub struct StremioServer {}
|
||||
|
||||
impl StremioServer {
|
||||
pub fn new() -> StremioServer {
|
||||
thread::spawn(move || {
|
||||
let job = Job::create().expect("Cannont create job");
|
||||
let mut info = job.query_extended_limit_info().expect("Cannont get info");
|
||||
info.limit_kill_on_job_close();
|
||||
job.set_extended_limit_info(&info).ok();
|
||||
job.assign_current_process().ok();
|
||||
loop {
|
||||
let child = Command::new("./stremio-runtime")
|
||||
.arg("server.js")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.spawn();
|
||||
match child {
|
||||
Ok(mut child) => {
|
||||
// TODO: store somehow last few lines of the child's stdout/stderr instead of just waiting
|
||||
child.wait().expect("Cannot wait for the server");
|
||||
}
|
||||
Err(err) => {
|
||||
nwg::error_message(
|
||||
"Stremio server",
|
||||
format!("Cannot execute stremio-runtime: {}", &err).as_str(),
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
// TODO: show error message with the child's stdout/stderr
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
dbg!("Trying to restart the server...");
|
||||
}
|
||||
});
|
||||
StremioServer {}
|
||||
}
|
||||
}
|
||||
2
src/stremio_app/stremio_wevbiew/mod.rs
Normal file
2
src/stremio_app/stremio_wevbiew/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod wevbiew;
|
||||
pub use wevbiew::WebView;
|
||||
205
src/stremio_app/stremio_wevbiew/wevbiew.rs
Normal file
205
src/stremio_app/stremio_wevbiew/wevbiew.rs
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
use crate::stremio_app::ipc;
|
||||
use native_windows_gui::{self as nwg, PartialUi};
|
||||
use once_cell::unsync::OnceCell;
|
||||
use serde_json::json;
|
||||
use std::borrow::Cow;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use urlencoding::decode;
|
||||
use webview2::Controller;
|
||||
use winapi::shared::windef::HWND;
|
||||
use winapi::um::winuser::{GetClientRect, WM_SETFOCUS};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WebView {
|
||||
pub disable_gpu: Rc<OnceCell<bool>>,
|
||||
pub endpoint: Rc<OnceCell<String>>,
|
||||
pub dev_tools: Rc<OnceCell<bool>>,
|
||||
pub controller: Rc<OnceCell<Controller>>,
|
||||
pub channel: ipc::Channel,
|
||||
notice: nwg::Notice,
|
||||
compute: RefCell<Option<thread::JoinHandle<()>>>,
|
||||
message_queue: Arc<Mutex<VecDeque<String>>>,
|
||||
}
|
||||
|
||||
impl WebView {
|
||||
pub fn fit_to_window(&self, hwnd: Option<HWND>) {
|
||||
if let Some(hwnd) = hwnd {
|
||||
unsafe {
|
||||
let mut rect = mem::zeroed();
|
||||
GetClientRect(hwnd, &mut rect);
|
||||
self.controller
|
||||
.get()
|
||||
.and_then(|controller| controller.put_bounds(rect).ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_to_window_bounds(controller: Option<&Controller>, hwnd: Option<HWND>) {
|
||||
if let (Some(controller), Some(hwnd)) = (controller, hwnd) {
|
||||
unsafe {
|
||||
let mut rect = mem::zeroed();
|
||||
GetClientRect(hwnd, &mut rect);
|
||||
controller.put_bounds(rect).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialUi for WebView {
|
||||
fn build_partial<W: Into<nwg::ControlHandle>>(
|
||||
data: &mut Self,
|
||||
parent: Option<W>,
|
||||
) -> Result<(), nwg::NwgError> {
|
||||
let (tx, rx) = flume::unbounded();
|
||||
let tx_drag_drop = tx.clone();
|
||||
let (tx_web, rx_web) = flume::unbounded();
|
||||
data.channel = RefCell::new(Some((tx, rx_web)));
|
||||
|
||||
let parent = parent.expect("No parent window").into();
|
||||
|
||||
let hwnd = parent.hwnd().expect("Cannot obtain window handle");
|
||||
nwg::Notice::builder()
|
||||
.parent(parent)
|
||||
.build(&mut data.notice)
|
||||
.ok();
|
||||
let controller_clone = data.controller.clone();
|
||||
let endpoint = data.endpoint.clone();
|
||||
let dev_tools = data.dev_tools.clone();
|
||||
let webview_flags = "--disable-web-security --autoplay-policy=no-user-gesture-required --disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection";
|
||||
let webview_flags = if *data.disable_gpu.get().unwrap() {
|
||||
format!("{} {}", webview_flags, "--disable-gpu")
|
||||
} else {
|
||||
webview_flags.to_string()
|
||||
};
|
||||
let result = webview2::EnvironmentBuilder::new()
|
||||
.with_additional_browser_arguments(&webview_flags)
|
||||
.build(move |env| {
|
||||
env.expect("Cannot obtain webview environment")
|
||||
.create_controller(hwnd, move |controller| {
|
||||
let controller = controller.expect("Cannot obtain webview controller");
|
||||
if let Ok(controller2) = controller.get_controller2() {
|
||||
controller2
|
||||
.put_default_background_color(webview2_sys::Color {
|
||||
r: 255,
|
||||
g: 255,
|
||||
b: 255,
|
||||
a: 0,
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
eprintln!("failed to get interface to controller2");
|
||||
}
|
||||
let webview = controller
|
||||
.get_webview()
|
||||
.expect("Cannot obtain webview from controller");
|
||||
let settings = webview.get_settings().unwrap();
|
||||
settings.put_is_status_bar_enabled(false).ok();
|
||||
settings.put_are_dev_tools_enabled(*dev_tools.get().unwrap()).ok();
|
||||
settings.put_are_default_context_menus_enabled(false).ok();
|
||||
settings.put_is_zoom_control_enabled(false).ok();
|
||||
settings.put_is_built_in_error_page_enabled(false).ok();
|
||||
if let Some(endpoint) = endpoint.get() {
|
||||
if webview
|
||||
.navigate(endpoint.as_str()).is_err() {
|
||||
tx_web.clone().send(ipc::RPCResponse::response_message(Some(json!(["app-error", format!("Cannot load WEB UI at '{}'", &endpoint)])))).ok();
|
||||
};
|
||||
}
|
||||
webview.execute_script(r##"
|
||||
try{console.log('Shell JS injected');if(window.self === window.top) {
|
||||
window.qt={webChannelTransport:{send:window.chrome.webview.postMessage}};
|
||||
window.chrome.webview.addEventListener('message',ev=>window.qt.webChannelTransport.onmessage(ev));
|
||||
window.onload=()=>{try{initShellComm();}catch(e){window.chrome.webview.postMessage('{"id":1,"args":["app-error","'+e.message+'"]}')}};
|
||||
}}catch(e){}
|
||||
"##, |_| Ok(())).expect("Cannot add script to webview");
|
||||
webview.add_web_message_received(move |_w, msg| {
|
||||
let msg = msg.try_get_web_message_as_string()?;
|
||||
tx_web.send(msg).ok();
|
||||
Ok(())
|
||||
}).expect("Cannot add web message received");
|
||||
webview.add_new_window_requested(move |_w, msg| {
|
||||
if let Some(file) = msg.get_uri().ok().and_then(|str| {decode(str.as_str()).ok().map(Cow::into_owned)}) {
|
||||
tx_drag_drop.send(ipc::RPCResponse::response_message(Some(json!(["dragdrop" ,[file]])))).ok();
|
||||
msg.put_handled(true).ok();
|
||||
}
|
||||
Ok(())
|
||||
}).expect("Cannot add D&D handler");
|
||||
|
||||
WebView::resize_to_window_bounds(Some(&controller), Some(hwnd));
|
||||
controller.put_is_visible(true).ok();
|
||||
controller
|
||||
.move_focus(webview2::MoveFocusReason::Programmatic)
|
||||
.ok();
|
||||
|
||||
controller_clone
|
||||
.set(controller)
|
||||
.expect("Cannot update the controller");
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
if let Err(e) = result {
|
||||
nwg::modal_fatal_message(
|
||||
parent,
|
||||
"Failed to Create WebView2 Environment",
|
||||
&format!("{}", e),
|
||||
);
|
||||
}
|
||||
|
||||
let sender = data.notice.sender();
|
||||
let message = data.message_queue.clone();
|
||||
*data.compute.borrow_mut() = Some(thread::spawn(move || loop {
|
||||
if let Ok(msg) = rx.recv() {
|
||||
let mut message = message.lock().unwrap();
|
||||
message.push_back(msg);
|
||||
sender.notice();
|
||||
}
|
||||
}));
|
||||
|
||||
// handler ids equal or smaller than 0xFFFF are reserved by NWG
|
||||
let handler_id = 0x10000;
|
||||
let controller_clone = data.controller.clone();
|
||||
nwg::bind_raw_event_handler(&parent, handler_id, move |_hwnd, msg, _w, _l| {
|
||||
if msg == WM_SETFOCUS {
|
||||
controller_clone.get().and_then(|controller| {
|
||||
controller
|
||||
.move_focus(webview2::MoveFocusReason::Programmatic)
|
||||
.ok()
|
||||
});
|
||||
}
|
||||
None
|
||||
})
|
||||
.ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn process_event<'a>(
|
||||
&self,
|
||||
evt: nwg::Event,
|
||||
_evt_data: &nwg::EventData,
|
||||
_handle: nwg::ControlHandle,
|
||||
) {
|
||||
use nwg::Event as E;
|
||||
match evt {
|
||||
E::OnWindowMinimize => {
|
||||
if let Some(controller) = self.controller.get() {
|
||||
controller.put_is_visible(false).ok();
|
||||
}
|
||||
}
|
||||
E::OnNotice => {
|
||||
let message_queue = self.message_queue.clone();
|
||||
if let Some(controller) = self.controller.get() {
|
||||
let webview = controller.get_webview().expect("Cannot get vebview");
|
||||
let mut message_queue = message_queue.lock().unwrap();
|
||||
for msg in message_queue.drain(..) {
|
||||
webview.post_web_message_as_string(msg.as_str()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/stremio_app/systray.rs
Normal file
28
src/stremio_app/systray.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
use native_windows_derive::NwgPartial;
|
||||
use native_windows_gui as nwg;
|
||||
|
||||
#[derive(Default, NwgPartial)]
|
||||
pub struct SystemTray {
|
||||
#[nwg_resource]
|
||||
pub embed: nwg::EmbedResource,
|
||||
#[nwg_resource(source_embed: Some(&data.embed), source_embed_str: Some("MAINICON"))]
|
||||
pub tray_icon: nwg::Icon,
|
||||
#[nwg_control(icon: Some(&data.tray_icon), tip: Some("Stremio"))]
|
||||
#[nwg_events(MousePressLeftUp: [Self::show_menu], OnContextMenu: [Self::show_menu])]
|
||||
pub tray: nwg::TrayNotification,
|
||||
#[nwg_control(popup: true)]
|
||||
pub tray_menu: nwg::Menu,
|
||||
#[nwg_control(parent: tray_menu, text: "&Show window")]
|
||||
pub tray_show_hide: nwg::MenuItem,
|
||||
#[nwg_control(parent: tray_menu, text: "Always on &top")]
|
||||
pub tray_topmost: nwg::MenuItem,
|
||||
#[nwg_control(parent: tray_menu, text: "&Quit")]
|
||||
pub tray_exit: nwg::MenuItem,
|
||||
}
|
||||
|
||||
impl SystemTray {
|
||||
fn show_menu(&self) {
|
||||
let (x, y) = nwg::GlobalCursor::position();
|
||||
self.tray_menu.popup(x, y);
|
||||
}
|
||||
}
|
||||
135
src/stremio_app/updater.rs
Normal file
135
src/stremio_app/updater.rs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
use std::{
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use semver::{Version, VersionReq};
|
||||
use serde::Deserialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Update {
|
||||
/// The new version that we update to
|
||||
pub version: Version,
|
||||
pub file: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Updater {
|
||||
pub current_version: Version,
|
||||
pub next_version: VersionReq,
|
||||
pub endpoint: Url,
|
||||
pub force_update: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UpdateResponse {
|
||||
version_desc: Url,
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FileItem {
|
||||
// name: String,
|
||||
pub url: Url,
|
||||
pub checksum: String,
|
||||
os: String,
|
||||
}
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Descriptor {
|
||||
version: String,
|
||||
files: Vec<FileItem>,
|
||||
}
|
||||
|
||||
impl Updater {
|
||||
pub fn new(current_version: Version, updater_endpoint: &Url, force_update: bool) -> Self {
|
||||
Self {
|
||||
next_version: VersionReq::parse(&format!(">{current_version}"))
|
||||
.expect("Version is type-safe"),
|
||||
current_version,
|
||||
endpoint: updater_endpoint.clone(),
|
||||
force_update,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches the latest update from the update server.
|
||||
pub fn autoupdate(&self) -> Result<Option<Update>, anyhow::Error> {
|
||||
// Check for updates
|
||||
println!("Fetching updates for v{}", self.current_version);
|
||||
println!("Using updater endpoint {}", &self.endpoint);
|
||||
let update_response =
|
||||
reqwest::blocking::get(self.endpoint.clone())?.json::<UpdateResponse>()?;
|
||||
let update_descriptor =
|
||||
reqwest::blocking::get(update_response.version_desc)?.json::<Descriptor>()?;
|
||||
|
||||
if update_response.version != update_descriptor.version {
|
||||
return Err(anyhow!("Mismatched update versions"));
|
||||
}
|
||||
let installer = update_descriptor
|
||||
.files
|
||||
.iter()
|
||||
.find(|file_item| file_item.os == std::env::consts::OS)
|
||||
.context("No update for this OS")?;
|
||||
let version = Version::parse(update_descriptor.version.as_str())?;
|
||||
if !self.force_update && !self.next_version.matches(&version) {
|
||||
return Err(anyhow!(
|
||||
"No new releases found that match the requirement of `{}`",
|
||||
self.next_version
|
||||
));
|
||||
}
|
||||
println!("Found update v{}", version);
|
||||
|
||||
// Download the new setup file
|
||||
let mut installer_response = reqwest::blocking::get(installer.url.clone())?;
|
||||
let size = installer_response.content_length();
|
||||
let mut downloaded: u64 = 0;
|
||||
let mut sha256 = Sha256::new();
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let file_name = std::path::Path::new(installer.url.path())
|
||||
.file_name()
|
||||
.context("Invalid file name")?
|
||||
.to_str()
|
||||
.context("The path is not valid UTF-8")?
|
||||
.to_string();
|
||||
|
||||
let dest = temp_dir.join(file_name);
|
||||
|
||||
println!("Downloading {} to {}", installer.url, dest.display());
|
||||
|
||||
let mut chunk = [0u8; 8192];
|
||||
let mut file = std::fs::File::create(&dest)?;
|
||||
loop {
|
||||
let chunk_size = installer_response.read(&mut chunk)?;
|
||||
if chunk_size == 0 {
|
||||
break;
|
||||
}
|
||||
sha256.update(&chunk[..chunk_size]);
|
||||
file.write_all(&chunk[..chunk_size])?;
|
||||
if let Some(size) = size {
|
||||
downloaded += chunk_size as u64;
|
||||
print!("\rProgress: {}%", downloaded * 100 / size);
|
||||
} else {
|
||||
print!(".");
|
||||
}
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
println!();
|
||||
let actual_sha256 = format!("{:x}", sha256.finalize());
|
||||
if actual_sha256 != installer.checksum {
|
||||
std::fs::remove_file(dest)?;
|
||||
return Err(anyhow::anyhow!("Checksum verification failed"));
|
||||
}
|
||||
println!("Checksum verified.");
|
||||
|
||||
let update = Some(Update {
|
||||
version,
|
||||
file: dest,
|
||||
});
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
144
src/stremio_app/window_helper.rs
Normal file
144
src/stremio_app/window_helper.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
use std::{cmp, mem};
|
||||
use winapi::shared::windef::HWND;
|
||||
use winapi::um::winuser::{
|
||||
GetForegroundWindow, GetSystemMetrics, GetWindowLongA, GetWindowRect, IsIconic, IsZoomed,
|
||||
SetForegroundWindow, SetWindowLongA, SetWindowPos, GWL_EXSTYLE, GWL_STYLE, HWND_NOTOPMOST,
|
||||
HWND_TOPMOST, SM_CXSCREEN, SM_CYSCREEN, SWP_FRAMECHANGED, SWP_NOMOVE, SWP_NOSIZE, WS_CAPTION,
|
||||
WS_EX_CLIENTEDGE, WS_EX_DLGMODALFRAME, WS_EX_STATICEDGE, WS_EX_TOPMOST, WS_EX_WINDOWEDGE,
|
||||
WS_THICKFRAME,
|
||||
};
|
||||
// https://doc.qt.io/qt-5/qt.html#WindowState-enum
|
||||
bitflags! {
|
||||
struct WindowState: u8 {
|
||||
const MINIMIZED = 0x01;
|
||||
const MAXIMIZED = 0x02;
|
||||
const FULL_SCREEN = 0x04;
|
||||
const ACTIVE = 0x08;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct WindowStyle {
|
||||
pub full_screen: bool,
|
||||
pub pos: (i32, i32),
|
||||
pub size: (i32, i32),
|
||||
pub style: i32,
|
||||
pub ex_style: i32,
|
||||
}
|
||||
|
||||
impl WindowStyle {
|
||||
pub fn get_window_state(self, hwnd: HWND) -> u32 {
|
||||
let mut state: WindowState = WindowState::empty();
|
||||
if 0 != unsafe { IsIconic(hwnd) } {
|
||||
state |= WindowState::MINIMIZED;
|
||||
}
|
||||
if 0 != unsafe { IsZoomed(hwnd) } {
|
||||
state |= WindowState::MAXIMIZED;
|
||||
}
|
||||
if hwnd == unsafe { GetForegroundWindow() } {
|
||||
state |= WindowState::ACTIVE
|
||||
}
|
||||
if self.full_screen {
|
||||
state |= WindowState::FULL_SCREEN;
|
||||
}
|
||||
state.bits() as u32
|
||||
}
|
||||
pub fn show_window_at(&self, hwnd: HWND, pos: HWND) {
|
||||
unsafe {
|
||||
SetWindowPos(
|
||||
hwnd,
|
||||
pos,
|
||||
self.pos.0,
|
||||
self.pos.1,
|
||||
self.size.0,
|
||||
self.size.1,
|
||||
SWP_FRAMECHANGED,
|
||||
);
|
||||
}
|
||||
}
|
||||
pub fn center_window(&mut self, hwnd: HWND, min_width: i32, min_height: i32) {
|
||||
let monitor_w = unsafe { GetSystemMetrics(SM_CXSCREEN) };
|
||||
let monitor_h = unsafe { GetSystemMetrics(SM_CYSCREEN) };
|
||||
let small_side = cmp::min(monitor_w, monitor_h) * 70 / 100;
|
||||
self.size = (
|
||||
cmp::max(small_side * 16 / 9, min_width),
|
||||
cmp::max(small_side, min_height),
|
||||
);
|
||||
self.pos = ((monitor_w - self.size.0) / 2, (monitor_h - self.size.1) / 2);
|
||||
self.show_window_at(hwnd, HWND_NOTOPMOST);
|
||||
}
|
||||
pub fn toggle_full_screen(&mut self, hwnd: HWND) {
|
||||
if self.full_screen {
|
||||
let topmost = if self.ex_style as u32 & WS_EX_TOPMOST == WS_EX_TOPMOST {
|
||||
HWND_TOPMOST
|
||||
} else {
|
||||
HWND_NOTOPMOST
|
||||
};
|
||||
unsafe {
|
||||
SetWindowLongA(hwnd, GWL_STYLE, self.style);
|
||||
SetWindowLongA(hwnd, GWL_EXSTYLE, self.ex_style);
|
||||
}
|
||||
self.show_window_at(hwnd, topmost);
|
||||
self.full_screen = false;
|
||||
} else {
|
||||
unsafe {
|
||||
let mut rect = mem::zeroed();
|
||||
GetWindowRect(hwnd, &mut rect);
|
||||
self.pos = (rect.left, rect.top);
|
||||
self.size = ((rect.right - rect.left), (rect.bottom - rect.top));
|
||||
self.style = GetWindowLongA(hwnd, GWL_STYLE);
|
||||
self.ex_style = GetWindowLongA(hwnd, GWL_EXSTYLE);
|
||||
SetWindowLongA(
|
||||
hwnd,
|
||||
GWL_STYLE,
|
||||
self.style & !(WS_CAPTION as i32 | WS_THICKFRAME as i32),
|
||||
);
|
||||
SetWindowLongA(
|
||||
hwnd,
|
||||
GWL_EXSTYLE,
|
||||
self.ex_style
|
||||
& !(WS_EX_DLGMODALFRAME as i32
|
||||
| WS_EX_WINDOWEDGE as i32
|
||||
| WS_EX_CLIENTEDGE as i32
|
||||
| WS_EX_STATICEDGE as i32),
|
||||
);
|
||||
SetWindowPos(
|
||||
hwnd,
|
||||
HWND_NOTOPMOST,
|
||||
0,
|
||||
0,
|
||||
GetSystemMetrics(SM_CXSCREEN),
|
||||
GetSystemMetrics(SM_CYSCREEN),
|
||||
SWP_FRAMECHANGED,
|
||||
);
|
||||
}
|
||||
self.full_screen = true;
|
||||
}
|
||||
}
|
||||
pub fn toggle_topmost(&mut self, hwnd: HWND) {
|
||||
let topmost = if unsafe { GetWindowLongA(hwnd, GWL_EXSTYLE) } as u32 & WS_EX_TOPMOST
|
||||
== WS_EX_TOPMOST
|
||||
{
|
||||
HWND_NOTOPMOST
|
||||
} else {
|
||||
HWND_TOPMOST
|
||||
};
|
||||
unsafe {
|
||||
SetWindowPos(
|
||||
hwnd,
|
||||
topmost,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED,
|
||||
);
|
||||
}
|
||||
self.ex_style = unsafe { GetWindowLongA(hwnd, GWL_EXSTYLE) };
|
||||
}
|
||||
pub fn set_active(&mut self, hwnd: HWND) {
|
||||
unsafe {
|
||||
SetForegroundWindow(hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue