mirror of
https://gitlab.com/vyla-entertainment/go.git
synced 2026-11-14 07:27:23 -05:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 23c2cd53c5 | |||
| 7b6fc838b8 | |||
| 60b24bf9e4 | |||
| 3958f751ee | |||
| 72b752fc02 | |||
| c7463364da | |||
| 084d215b23 | |||
| a4b3a77153 | |||
|
|
9b9d0dfc49 | ||
|
|
7f7a969698 | ||
|
|
fd738eeba5 | ||
|
|
a28239cf60 | ||
|
|
966115c456 | ||
| 2c0cc5c933 | |||
| 0e385aae9e | |||
|
cd18f09142
|
|||
| bd321520c9 | |||
| c869101923 | |||
| feccab9aa1 | |||
| 82c91334f6 |
@@ -65,3 +65,8 @@ Force-stop and clear app data:
|
|||||||
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" shell am force-stop com.vyla.go
|
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" shell am force-stop com.vyla.go
|
||||||
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" shell pm clear com.vyla.go
|
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" shell pm clear com.vyla.go
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Fix emulator DNS timeouts (if emulator gets network timeouts connecting to GitLab):
|
||||||
|
```powershell
|
||||||
|
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" shell settings put global private_dns_mode off
|
||||||
|
```
|
||||||
|
|||||||
@@ -20,33 +20,91 @@ function cleanBuildDir() {
|
|||||||
fs.mkdirSync(BUILD_DIR, { recursive: true });
|
fs.mkdirSync(BUILD_DIR, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeSensitiveFiles(dir) {
|
||||||
|
if (!fs.existsSync(dir)) return;
|
||||||
|
|
||||||
|
const gitDir = path.join(dir, '.git');
|
||||||
|
if (fs.existsSync(gitDir)) {
|
||||||
|
fs.rmSync(gitDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (entry.name === '.git') {
|
||||||
|
fs.rmSync(fullPath, { recursive: true, force: true });
|
||||||
|
} else {
|
||||||
|
removeSensitiveFiles(fullPath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
entry.name.startsWith('.env') ||
|
||||||
|
entry.name.endsWith('.key') ||
|
||||||
|
entry.name.endsWith('.pem') ||
|
||||||
|
entry.name === 'credentials.json' ||
|
||||||
|
entry.name === '.gitlab-ci.yml'
|
||||||
|
) {
|
||||||
|
fs.rmSync(fullPath, { force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function clonePrivateRepo(repoConfig, targetDir) {
|
function clonePrivateRepo(repoConfig, targetDir) {
|
||||||
if (fs.existsSync(targetDir)) {
|
if (fs.existsSync(targetDir)) {
|
||||||
fs.rmSync(targetDir, { recursive: true, force: true });
|
fs.rmSync(targetDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = process.env.GITLAB_TOKEN || process.env.VYLA_GITLAB_TOKEN;
|
const token = process.env.GITLAB_TOKEN || process.env.VYLA_GITLAB_TOKEN;
|
||||||
const auth = token ? `oauth2:${token}@` : '';
|
const repoUrl = `https://gitlab.com/${GITLAB_GROUP}/${repoConfig.name}.git`;
|
||||||
const repoUrl = `https://${auth}gitlab.com/${GITLAB_GROUP}/${repoConfig.name}.git`;
|
|
||||||
execSync(`git clone --depth 1 --branch ${repoConfig.branch} ${repoUrl} ${targetDir}`, { stdio: 'inherit' });
|
try {
|
||||||
|
if (token) {
|
||||||
|
const authHeader = Buffer.from(`oauth2:${token.trim()}`).toString('base64');
|
||||||
|
execSync(
|
||||||
|
`git -c "http.extraHeader=Authorization: Basic ${authHeader}" clone --depth 1 --branch ${repoConfig.branch} ${repoUrl} "${targetDir}"`,
|
||||||
|
{ stdio: 'inherit' }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
execSync(
|
||||||
|
`git clone --depth 1 --branch ${repoConfig.branch} ${repoUrl} "${targetDir}"`,
|
||||||
|
{ stdio: 'inherit' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
const gitFolder = path.join(targetDir, '.git');
|
||||||
|
if (fs.existsSync(gitFolder)) {
|
||||||
|
fs.rmSync(gitFolder, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function build() {
|
async function build() {
|
||||||
cleanBuildDir();
|
cleanBuildDir();
|
||||||
|
|
||||||
for (const [key, config] of Object.entries(REPO_CONFIG)) {
|
for (const [key, config] of Object.entries(REPO_CONFIG)) {
|
||||||
const targetDir = path.join(__dirname, key);
|
const targetDir = path.join(__dirname, key);
|
||||||
|
console.log(`Cloning ${config.name}...`);
|
||||||
clonePrivateRepo(config, targetDir);
|
clonePrivateRepo(config, targetDir);
|
||||||
|
|
||||||
|
console.log(`Installing production dependencies for ${config.name}...`);
|
||||||
execSync('npm install --omit=dev', { cwd: targetDir, stdio: 'inherit' });
|
execSync('npm install --omit=dev', { cwd: targetDir, stdio: 'inherit' });
|
||||||
|
|
||||||
|
removeSensitiveFiles(targetDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
const zip = new AdmZip();
|
const zip = new AdmZip();
|
||||||
|
|
||||||
const configDir = path.join(__dirname, 'config');
|
const configDir = path.join(__dirname, 'config');
|
||||||
if (fs.existsSync(configDir)) {
|
if (fs.existsSync(configDir)) {
|
||||||
|
removeSensitiveFiles(configDir);
|
||||||
zip.addLocalFolder(configDir, 'config');
|
zip.addLocalFolder(configDir, 'config');
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const repo of REPOS) {
|
for (const repo of REPOS) {
|
||||||
const srcRepo = path.join(__dirname, repo);
|
const srcRepo = path.join(__dirname, repo);
|
||||||
|
removeSensitiveFiles(srcRepo);
|
||||||
zip.addLocalFolder(srcRepo, repo);
|
zip.addLocalFolder(srcRepo, repo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +112,20 @@ async function build() {
|
|||||||
if (fs.existsSync(zipPath)) fs.rmSync(zipPath, { force: true });
|
if (fs.existsSync(zipPath)) fs.rmSync(zipPath, { force: true });
|
||||||
zip.writeZip(zipPath);
|
zip.writeZip(zipPath);
|
||||||
|
|
||||||
|
const verificationZip = new AdmZip(zipPath);
|
||||||
|
const leakedEntries = verificationZip.getEntries().filter(e =>
|
||||||
|
e.entryName.includes('.git/') ||
|
||||||
|
e.entryName.includes('.git\\') ||
|
||||||
|
e.entryName.startsWith('.env') ||
|
||||||
|
e.entryName.includes('/.env') ||
|
||||||
|
e.entryName.endsWith('config/encrypted-credentials.json')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (leakedEntries.length > 0) {
|
||||||
|
fs.rmSync(zipPath, { force: true });
|
||||||
|
throw new Error(`SECURITY ABORT: Archive contains forbidden files:\n${leakedEntries.map(e => e.entryName).join('\n')}`);
|
||||||
|
}
|
||||||
|
|
||||||
const hash = crypto.createHash('sha256');
|
const hash = crypto.createHash('sha256');
|
||||||
hash.update(fs.readFileSync(zipPath));
|
hash.update(fs.readFileSync(zipPath));
|
||||||
const checksum = hash.digest('hex');
|
const checksum = hash.digest('hex');
|
||||||
@@ -68,6 +140,8 @@ async function build() {
|
|||||||
for (const repo of REPOS) {
|
for (const repo of REPOS) {
|
||||||
fs.rmSync(path.join(__dirname, repo), { recursive: true, force: true });
|
fs.rmSync(path.join(__dirname, repo), { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Build complete and verified clean.');
|
||||||
}
|
}
|
||||||
|
|
||||||
build().catch(err => {
|
build().catch(err => {
|
||||||
|
|||||||
1
frontend
1
frontend
Submodule frontend deleted from e23979cbe0
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vyla-go",
|
"name": "vyla-go",
|
||||||
"version": "1.0.3",
|
"version": "1.0.15",
|
||||||
"description": "Vyla Go Mobile Launcher",
|
"description": "Vyla Go Mobile Launcher",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"@tauri-apps/plugin-opener": "^2.0.0",
|
"@tauri-apps/plugin-opener": "^2.0.0",
|
||||||
"@tauri-apps/plugin-process": "^2.0.0",
|
"@tauri-apps/plugin-process": "^2.0.0",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||||
"@vyla-entertainment/sdk": "^1.11.0",
|
"@vyla-entertainment/sdk": "^1.18.0",
|
||||||
"adm-zip": "^0.6.0",
|
"adm-zip": "^0.6.0",
|
||||||
"express": "^4.19.2"
|
"express": "^4.19.2"
|
||||||
},
|
},
|
||||||
|
|||||||
22
release.js
22
release.js
@@ -32,11 +32,21 @@ function incrementVersion() {
|
|||||||
function gitCommitAndTag(version) {
|
function gitCommitAndTag(version) {
|
||||||
try {
|
try {
|
||||||
execSync('git add package.json src-tauri/tauri.conf.json', { stdio: 'inherit' });
|
execSync('git add package.json src-tauri/tauri.conf.json', { stdio: 'inherit' });
|
||||||
execSync(`git commit -m "chore: release ${version}"`, { stdio: 'inherit' });
|
try {
|
||||||
execSync(`git tag -a ${version} -m "Release ${version}"`, { stdio: 'inherit' });
|
execSync(`git commit -m "chore: release ${version}"`, { stdio: 'inherit' });
|
||||||
|
} catch (_) {
|
||||||
|
console.log('Working tree clean, skipping commit.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(`git tag -a ${version} -m "Release ${version}"`, { stdio: 'inherit' });
|
||||||
|
} catch (_) {
|
||||||
|
console.log(`Tag ${version} already exists locally.`);
|
||||||
|
}
|
||||||
|
|
||||||
execSync('git push origin HEAD --tags', { stdio: 'inherit' });
|
execSync('git push origin HEAD --tags', { stdio: 'inherit' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Git operations failed (non-fatal):', e.message);
|
console.warn('Git push failed (will fallback to GitLab API ref):', e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +104,11 @@ async function createRelease(version, assets) {
|
|||||||
const payload = assets.payload;
|
const payload = assets.payload;
|
||||||
const manifest = assets.manifest;
|
const manifest = assets.manifest;
|
||||||
|
|
||||||
|
let branch = 'main';
|
||||||
|
try {
|
||||||
|
branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim() || 'main';
|
||||||
|
} catch (_) { }
|
||||||
|
|
||||||
const description = [
|
const description = [
|
||||||
`## Automated Release ${version}`,
|
`## Automated Release ${version}`,
|
||||||
'',
|
'',
|
||||||
@@ -110,6 +125,7 @@ async function createRelease(version, assets) {
|
|||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
name: `Vyla Go Release ${version}`,
|
name: `Vyla Go Release ${version}`,
|
||||||
tag_name: tag,
|
tag_name: tag,
|
||||||
|
ref: branch,
|
||||||
description,
|
description,
|
||||||
assets: {
|
assets: {
|
||||||
links: [
|
links: [
|
||||||
|
|||||||
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -4593,6 +4593,7 @@ dependencies = [
|
|||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"tauri-plugin-process",
|
"tauri-plugin-process",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
|
"tokio",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -23,3 +23,4 @@ reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"
|
|||||||
zip = { version = "2.2", default-features = false, features = ["deflate"] }
|
zip = { version = "2.2", default-features = false, features = ["deflate"] }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
urlencoding = "2.1"
|
urlencoding = "2.1"
|
||||||
|
tokio = { version = "1", features = ["time"] }
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ val tauriProperties = Properties().apply {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configurations.all {
|
||||||
|
resolutionStrategy {
|
||||||
|
force("com.fasterxml.jackson.core:jackson-databind:2.13.5")
|
||||||
|
force("com.fasterxml.jackson.core:jackson-core:2.13.5")
|
||||||
|
force("com.fasterxml.jackson.core:jackson-annotations:2.13.5")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
namespace = "com.vyla.go"
|
namespace = "com.vyla.go"
|
||||||
@@ -23,6 +31,9 @@ android {
|
|||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||||
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||||
|
ndk {
|
||||||
|
abiFilters.addAll(listOf("arm64-v8a", "armeabi-v7a", "x86_64"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("release") {
|
create("release") {
|
||||||
@@ -59,6 +70,11 @@ android {
|
|||||||
useLegacyPackaging = true
|
useLegacyPackaging = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rust {
|
rust {
|
||||||
@@ -74,6 +90,7 @@ dependencies {
|
|||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(from = "tauri.build.gradle.kts")
|
apply(from = "tauri.build.gradle.kts")
|
||||||
@@ -2,15 +2,19 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||||
|
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:banner="@drawable/tv_banner"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:extractNativeLibs="true"
|
android:extractNativeLibs="true"
|
||||||
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.vyla_go">
|
android:theme="@style/Theme.vyla_go">
|
||||||
<activity
|
<activity
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||||
@@ -22,6 +26,10 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,36 +1,73 @@
|
|||||||
package com.vyla.go
|
package com.vyla.go
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.WindowManager
|
||||||
import android.view.ViewGroup
|
import android.webkit.JavascriptInterface
|
||||||
import android.webkit.WebChromeClient
|
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
|
||||||
class MainActivity : TauriActivity() {
|
class MainActivity : TauriActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
private var webView: WebView? = null
|
||||||
super.onCreate(savedInstanceState)
|
private var insetTop = 0f
|
||||||
|
private var insetRight = 0f
|
||||||
|
private var insetBottom = 0f
|
||||||
|
private var insetLeft = 0f
|
||||||
|
|
||||||
val root = findViewById<ViewGroup>(android.R.id.content)
|
private inner class InsetBridge {
|
||||||
root.viewTreeObserver.addOnGlobalLayoutListener {
|
@JavascriptInterface
|
||||||
findAndHookWebView(root)
|
fun get(): String =
|
||||||
}
|
"{\"top\":$insetTop,\"right\":$insetRight,\"bottom\":$insetBottom,\"left\":$insetLeft}"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findAndHookWebView(view: View) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
if (view is WebView) {
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
view.webChromeClient = object : WebChromeClient() {
|
window.statusBarColor = Color.TRANSPARENT
|
||||||
override fun getDefaultVideoPoster(): Bitmap {
|
window.navigationBarColor = Color.TRANSPARENT
|
||||||
return Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
}
|
window.isNavigationBarContrastEnforced = false
|
||||||
}
|
window.isStatusBarContrastEnforced = false
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
window.attributes.layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
|
}
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
WindowInsetsControllerCompat(window, window.decorView).apply {
|
||||||
|
isAppearanceLightStatusBars = false
|
||||||
|
isAppearanceLightNavigationBars = false
|
||||||
|
}
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { _, insets ->
|
||||||
|
val bars = insets.getInsets(
|
||||||
|
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
|
||||||
|
)
|
||||||
|
val d = resources.displayMetrics.density
|
||||||
|
insetTop = bars.top / d
|
||||||
|
insetRight = bars.right / d
|
||||||
|
insetBottom = bars.bottom / d
|
||||||
|
insetLeft = bars.left / d
|
||||||
|
pushInsets()
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
ViewCompat.requestApplyInsets(window.decorView)
|
||||||
|
}
|
||||||
|
|
||||||
if (view is ViewGroup) {
|
override fun onWebViewCreate(webView: WebView) {
|
||||||
for (i in 0 until view.childCount) {
|
this.webView = webView
|
||||||
findAndHookWebView(view.getChildAt(i))
|
webView.addJavascriptInterface(InsetBridge(), "VylaInsets")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun pushInsets() {
|
||||||
|
val wv = webView ?: return
|
||||||
|
wv.post {
|
||||||
|
wv.evaluateJavascript(
|
||||||
|
"window.__vylaApplyInsets&&window.__vylaApplyInsets($insetTop,$insetRight,$insetBottom,$insetLeft)",
|
||||||
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BIN
src-tauri/gen/android/app/src/main/res/drawable/tv_banner.png
Normal file
BIN
src-tauri/gen/android/app/src/main/res/drawable/tv_banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
@@ -21,7 +21,6 @@ open class BuildTask : DefaultTask() {
|
|||||||
runTauriCli(executable)
|
runTauriCli(executable)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||||
// Try different Windows-specific extensions
|
|
||||||
val fallbacks = listOf(
|
val fallbacks = listOf(
|
||||||
"$executable.exe",
|
"$executable.exe",
|
||||||
"$executable.cmd",
|
"$executable.cmd",
|
||||||
|
|||||||
2875
src-tauri/gen/schemas/desktop-schema.json
Normal file
2875
src-tauri/gen/schemas/desktop-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
2875
src-tauri/gen/schemas/windows-schema.json
Normal file
2875
src-tauri/gen/schemas/windows-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,135 @@ const RUNTIME_INTERCEPT_SCRIPT: &str = r#"
|
|||||||
})();
|
})();
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
const NODE_PRELOAD_SCRIPT: &str = r#"
|
||||||
|
const Module = require('module');
|
||||||
|
const origLoad = Module._load;
|
||||||
|
|
||||||
|
const stub = new Proxy(function () {}, {
|
||||||
|
get: (t, k) => (typeof k === 'symbol' || k === 'then' ? undefined : stub),
|
||||||
|
apply: () => stub,
|
||||||
|
construct: () => stub,
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildTlsShim() {
|
||||||
|
class Session {
|
||||||
|
constructor(opts) {
|
||||||
|
this.opts = opts || {};
|
||||||
|
this.headers = this.opts.headers || {};
|
||||||
|
}
|
||||||
|
async request(method, url, options) {
|
||||||
|
options = options || {};
|
||||||
|
const headers = Object.assign({}, this.headers, options.headers || {});
|
||||||
|
const init = {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
redirect: options.followRedirects === false ? 'manual' : 'follow',
|
||||||
|
};
|
||||||
|
let body = options.body !== undefined ? options.body : options.data;
|
||||||
|
if (options.json !== undefined) {
|
||||||
|
body = JSON.stringify(options.json);
|
||||||
|
if (!headers['content-type'] && !headers['Content-Type']) {
|
||||||
|
headers['content-type'] = 'application/json';
|
||||||
|
}
|
||||||
|
} else if (body !== undefined && body !== null && typeof body === 'object' && !Buffer.isBuffer(body) && !(body instanceof URLSearchParams)) {
|
||||||
|
body = new URLSearchParams(body).toString();
|
||||||
|
if (!headers['content-type'] && !headers['Content-Type']) {
|
||||||
|
headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body !== undefined && body !== null && method !== 'GET' && method !== 'HEAD') {
|
||||||
|
init.body = body;
|
||||||
|
}
|
||||||
|
let ms = Number(options.timeout || this.opts.timeout || 30000);
|
||||||
|
if (ms < 1000) ms = ms * 1000;
|
||||||
|
init.signal = AbortSignal.timeout(ms);
|
||||||
|
const res = await fetch(url, init);
|
||||||
|
const text = await res.text();
|
||||||
|
const outHeaders = {};
|
||||||
|
res.headers.forEach((v, k) => { outHeaders[k] = v; });
|
||||||
|
return {
|
||||||
|
id: String(Date.now()),
|
||||||
|
status: res.status,
|
||||||
|
ok: res.ok,
|
||||||
|
success: res.ok,
|
||||||
|
url: res.url,
|
||||||
|
target: res.url,
|
||||||
|
headers: outHeaders,
|
||||||
|
cookies: {},
|
||||||
|
body: text,
|
||||||
|
text: async () => text,
|
||||||
|
json: async () => JSON.parse(text),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
get(url, o) { return this.request('GET', url, o); }
|
||||||
|
post(url, o) { return this.request('POST', url, o); }
|
||||||
|
put(url, o) { return this.request('PUT', url, o); }
|
||||||
|
patch(url, o) { return this.request('PATCH', url, o); }
|
||||||
|
delete(url, o) { return this.request('DELETE', url, o); }
|
||||||
|
head(url, o) { return this.request('HEAD', url, o); }
|
||||||
|
options(url, o) { return this.request('OPTIONS', url, o); }
|
||||||
|
async close() {}
|
||||||
|
async free() {}
|
||||||
|
}
|
||||||
|
const ClientIdentifier = new Proxy({}, { get: (t, k) => (typeof k === 'symbol' ? undefined : String(k)) });
|
||||||
|
const mod = {
|
||||||
|
Session,
|
||||||
|
ClientIdentifier,
|
||||||
|
initTLS: async () => {},
|
||||||
|
destroyTLS: async () => {},
|
||||||
|
};
|
||||||
|
mod.default = mod;
|
||||||
|
return mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tlsShim = null;
|
||||||
|
|
||||||
|
Module._load = function (request) {
|
||||||
|
if (request === 'node-tls-client') {
|
||||||
|
if (!tlsShim) {
|
||||||
|
tlsShim = buildTlsShim();
|
||||||
|
console.error('node-tls-client replaced with fetch shim');
|
||||||
|
}
|
||||||
|
return tlsShim;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return origLoad.apply(this, arguments);
|
||||||
|
} catch (e) {
|
||||||
|
const msg = String(e && e.message);
|
||||||
|
if (/koffi/i.test(request) || /koffi/i.test(msg)) {
|
||||||
|
console.error('stubbed native module: ' + request + ' (' + msg + ')');
|
||||||
|
return stub;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('uncaughtException', (e) => console.error('uncaughtException', e && e.stack || e));
|
||||||
|
process.on('unhandledRejection', (e) => console.error('unhandledRejection', e && e.stack || e));
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const LAUNCHER_SCRIPT: &str = r#"
|
||||||
|
const path = require('path');
|
||||||
|
const { pathToFileURL } = require('url');
|
||||||
|
|
||||||
|
async function boot(file, port) {
|
||||||
|
process.env.PORT = String(port);
|
||||||
|
try {
|
||||||
|
await import(pathToFileURL(file).href);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('failed to start ' + file, e && e.stack || e);
|
||||||
|
} finally {
|
||||||
|
process.env.PORT = '3000';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const root = process.env.VYLA_ROOT;
|
||||||
|
await boot(path.join(root, 'player', 'server.js'), 3000);
|
||||||
|
await boot(path.join(root, 'frontend', 'server.js'), 3001);
|
||||||
|
})();
|
||||||
|
"#;
|
||||||
|
|
||||||
fn android_log(msg: &str) {
|
fn android_log(msg: &str) {
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
{
|
{
|
||||||
@@ -135,6 +264,13 @@ fn spawn_service_manager(base_dir: &std::path::Path) {
|
|||||||
.map(|d| d.to_string_lossy().to_string())
|
.map(|d| d.to_string_lossy().to_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let tmp_dir = base_dir.join("tmp");
|
||||||
|
let _ = std::fs::create_dir_all(&tmp_dir);
|
||||||
|
std::env::set_var("HOME", base_dir.to_string_lossy().to_string());
|
||||||
|
std::env::set_var("TMPDIR", tmp_dir.to_string_lossy().to_string());
|
||||||
|
let preload_path = base_dir.join("preload.js");
|
||||||
|
let _ = std::fs::write(&preload_path, NODE_PRELOAD_SCRIPT);
|
||||||
|
let preload_str = preload_path.to_string_lossy().to_string();
|
||||||
std::env::set_var("PORT", "3000");
|
std::env::set_var("PORT", "3000");
|
||||||
std::env::set_var("NODE_ENV", "production");
|
std::env::set_var("NODE_ENV", "production");
|
||||||
std::env::set_var("TMDB_API_KEY", "3f6703f3e2a0b2b40fe4fbb3e1546244");
|
std::env::set_var("TMDB_API_KEY", "3f6703f3e2a0b2b40fe4fbb3e1546244");
|
||||||
@@ -147,29 +283,39 @@ fn spawn_service_manager(base_dir: &std::path::Path) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let server_js = server_script.to_string_lossy().to_string();
|
let launcher_path = base_dir.join("launcher.js");
|
||||||
|
let _ = std::fs::write(&launcher_path, LAUNCHER_SCRIPT);
|
||||||
|
std::env::set_var("VYLA_ROOT", base_dir.to_string_lossy().to_string());
|
||||||
|
let server_js = launcher_path.to_string_lossy().to_string();
|
||||||
let player_dir_str = player_dir.to_string_lossy().to_string();
|
let player_dir_str = player_dir.to_string_lossy().to_string();
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
let _ = std::thread::Builder::new().stack_size(16 * 1024 * 1024).spawn(move || {
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
{
|
{
|
||||||
let _ = std::env::set_current_dir(&player_dir_str);
|
let _ = std::env::set_current_dir(&player_dir_str);
|
||||||
|
|
||||||
let arg0 = std::ffi::CString::new("node").unwrap();
|
let parts: [&str; 4] = ["node", "--require", preload_str.as_str(), server_js.as_str()];
|
||||||
let arg1 = std::ffi::CString::new(server_js.as_str()).unwrap();
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
let mut offsets: Vec<usize> = Vec::new();
|
||||||
|
for p in parts.iter() {
|
||||||
|
offsets.push(buf.len());
|
||||||
|
buf.extend_from_slice(p.as_bytes());
|
||||||
|
buf.push(0);
|
||||||
|
}
|
||||||
|
buf.shrink_to_fit();
|
||||||
|
|
||||||
let mut argv: Vec<*mut std::os::raw::c_char> = vec![
|
let base = buf.as_mut_ptr() as *mut std::os::raw::c_char;
|
||||||
arg0.as_ptr() as *mut _,
|
let mut argv: Vec<*mut std::os::raw::c_char> = offsets
|
||||||
arg1.as_ptr() as *mut _,
|
.iter()
|
||||||
std::ptr::null_mut(),
|
.map(|o| unsafe { base.add(*o) })
|
||||||
];
|
.collect();
|
||||||
|
argv.push(std::ptr::null_mut());
|
||||||
|
|
||||||
|
std::mem::forget(buf);
|
||||||
|
|
||||||
android_log("calling node::Start via FFI");
|
android_log("calling node::Start via FFI");
|
||||||
let ret = unsafe { node_ffi::_ZN4node5StartEiPPc(2, argv.as_mut_ptr()) };
|
let ret = unsafe { node_ffi::_ZN4node5StartEiPPc(4, argv.as_mut_ptr()) };
|
||||||
android_log(&format!("node::Start returned: {}", ret));
|
android_log(&format!("node::Start returned: {}", ret));
|
||||||
|
|
||||||
drop(arg0);
|
|
||||||
drop(arg1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
@@ -179,6 +325,7 @@ fn spawn_service_manager(base_dir: &std::path::Path) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let _ = wait_for_port(3000, Duration::from_secs(45));
|
let _ = wait_for_port(3000, Duration::from_secs(45));
|
||||||
|
let _ = wait_for_port(3001, Duration::from_secs(45));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_directory_replacements(dir: &std::path::Path, find_val: &str, replace_val: &str) {
|
fn apply_directory_replacements(dir: &std::path::Path, find_val: &str, replace_val: &str) {
|
||||||
@@ -231,44 +378,160 @@ async fn download_and_extract_payload(app: tauri::AppHandle, window: WebviewWind
|
|||||||
let base_dir = get_app_dir(&app);
|
let base_dir = get_app_dir(&app);
|
||||||
std::fs::create_dir_all(&base_dir).map_err(|e| format!("Storage dir error: {}", e))?;
|
std::fs::create_dir_all(&base_dir).map_err(|e| format!("Storage dir error: {}", e))?;
|
||||||
|
|
||||||
|
let marker = base_dir.join(".installed");
|
||||||
let frontend_index = base_dir.join("frontend").join("index.html");
|
let frontend_index = base_dir.join("frontend").join("index.html");
|
||||||
if frontend_index.exists() {
|
let server_script = base_dir.join("player").join("server.js");
|
||||||
|
let frontend_server = base_dir.join("frontend").join("server.js");
|
||||||
|
if marker.exists() && frontend_index.exists() && server_script.exists() && frontend_server.exists() {
|
||||||
|
android_log("App already installed, launching...");
|
||||||
apply_patches(&base_dir);
|
apply_patches(&base_dir);
|
||||||
spawn_service_manager(&base_dir);
|
spawn_service_manager(&base_dir);
|
||||||
update_ui(&window, "Launching Vyla Go...", Some(100));
|
update_ui(&window, "Launching Vyla Go...", Some(100));
|
||||||
return Ok(base_dir);
|
return Ok(base_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
update_ui(&window, "Connecting to GitLab...", Some(10));
|
let local_payload_candidates = [
|
||||||
|
base_dir.join("update-payload.zip"),
|
||||||
|
base_dir.join("temp-payload.zip"),
|
||||||
|
PathBuf::from("/sdcard/Download/update-payload.zip"),
|
||||||
|
PathBuf::from("/data/local/tmp/update-payload.zip"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for candidate in &local_payload_candidates {
|
||||||
|
if candidate.exists() {
|
||||||
|
if let Ok(meta) = std::fs::metadata(candidate) {
|
||||||
|
if meta.len() > 10_000_000 {
|
||||||
|
if let Ok(f) = File::open(candidate) {
|
||||||
|
if let Ok(archive) = zip::ZipArchive::new(f) {
|
||||||
|
if archive.len() > 0 {
|
||||||
|
android_log(&format!("Found valid local payload: {}", candidate.display()));
|
||||||
|
update_ui(&window, "Unpacking local package...", Some(80));
|
||||||
|
let extract_dir = base_dir.clone();
|
||||||
|
let candidate_path = candidate.clone();
|
||||||
|
let extract_ok = tauri::async_runtime::spawn_blocking(move || -> Result<(), String> {
|
||||||
|
let zip_file = File::open(&candidate_path).map_err(|e| format!("Open zip error: {}", e))?;
|
||||||
|
let mut archive = zip::ZipArchive::new(zip_file).map_err(|e| format!("Zip parse error: {}", e))?;
|
||||||
|
archive.extract(&extract_dir).map_err(|e| format!("Extraction error: {}", e))?;
|
||||||
|
let _ = std::fs::write(extract_dir.join(".installed"), "ok");
|
||||||
|
apply_patches(&extract_dir);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
if extract_ok.is_ok() {
|
||||||
|
spawn_service_manager(&base_dir);
|
||||||
|
update_ui(&window, "Launching Vyla Go...", Some(100));
|
||||||
|
return Ok(base_dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.user_agent("Mozilla/5.0 (Android) VylaGo/1.0")
|
.user_agent("Mozilla/5.0 (Android) VylaGo/1.0")
|
||||||
.connect_timeout(std::time::Duration::from_secs(30))
|
.connect_timeout(std::time::Duration::from_secs(15))
|
||||||
|
.timeout(std::time::Duration::from_secs(180))
|
||||||
.redirect(reqwest::redirect::Policy::limited(10))
|
.redirect(reqwest::redirect::Policy::limited(10))
|
||||||
|
.local_address(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED))
|
||||||
|
.http1_only()
|
||||||
|
.pool_max_idle_per_host(0)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| format!("Client error: {}", e))?;
|
.map_err(|e| format!("Client error: {}", e))?;
|
||||||
|
|
||||||
let releases_url = "https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases";
|
let release_endpoints = [
|
||||||
let res = client.get(releases_url).send().await.map_err(|e| format!("Release check failed: {}", e))?;
|
"https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/releases",
|
||||||
let json: serde_json::Value = res.json().await.map_err(|e| format!("JSON error: {}", e))?;
|
"https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases",
|
||||||
|
];
|
||||||
|
|
||||||
let download_url = json.get(0)
|
let mut download_url: Option<String> = None;
|
||||||
.and_then(|r| r.get("assets"))
|
let mut last_error = String::new();
|
||||||
.and_then(|a| a.get("links"))
|
|
||||||
.and_then(|l| l.as_array())
|
const MAX_RETRIES: u32 = 4;
|
||||||
.and_then(|links| {
|
for attempt in 1..=MAX_RETRIES {
|
||||||
links.iter().find(|item| {
|
let msg = if attempt == 1 {
|
||||||
item.get("name").and_then(|n| n.as_str()) == Some("update-payload.zip")
|
"Connecting to GitLab...".to_string()
|
||||||
})
|
} else {
|
||||||
})
|
format!("Connecting to GitLab (attempt {}/{})...", attempt, MAX_RETRIES)
|
||||||
.and_then(|asset| asset.get("url"))
|
};
|
||||||
.and_then(|u| u.as_str())
|
update_ui(&window, &msg, Some(10 + (attempt - 1) * 3));
|
||||||
.ok_or_else(|| "Could not locate update-payload.zip link in release assets".to_string())?;
|
|
||||||
|
for endpoint in &release_endpoints {
|
||||||
|
android_log(&format!("Checking endpoint: {} (attempt {})", endpoint, attempt));
|
||||||
|
match client.get(*endpoint).send().await {
|
||||||
|
Ok(res) => {
|
||||||
|
if res.status().is_success() {
|
||||||
|
if let Ok(json) = res.json::<serde_json::Value>().await {
|
||||||
|
if let Some(releases) = json.as_array() {
|
||||||
|
for rel in releases {
|
||||||
|
if let Some(links) = rel.get("assets").and_then(|a| a.get("links")).and_then(|l| l.as_array()) {
|
||||||
|
for item in links {
|
||||||
|
if item.get("name").and_then(|n| n.as_str()) == Some("update-payload.zip") {
|
||||||
|
if let Some(url) = item.get("url").and_then(|u| u.as_str()) {
|
||||||
|
download_url = Some(url.to_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if download_url.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
last_error = format!("HTTP {}", res.status());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let mut detail = format!("{}", e);
|
||||||
|
let mut src = StdError::source(&e);
|
||||||
|
while let Some(s) = src {
|
||||||
|
detail.push_str(&format!(" <- {}", s));
|
||||||
|
src = s.source();
|
||||||
|
}
|
||||||
|
android_log(&format!("Release check failed for {}: {}", endpoint, detail));
|
||||||
|
last_error = detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if download_url.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if download_url.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempt < MAX_RETRIES {
|
||||||
|
let backoff = attempt * 2;
|
||||||
|
update_ui(
|
||||||
|
&window,
|
||||||
|
&format!("Connection pending, retrying in {}s...", backoff),
|
||||||
|
Some(10 + attempt * 3),
|
||||||
|
);
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(backoff as u64)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let download_url = download_url.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"Could not connect to release server ({})",
|
||||||
|
if last_error.is_empty() { "timeout" } else { &last_error }
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
update_ui(&window, "Downloading update payload...", Some(25));
|
update_ui(&window, "Downloading update payload...", Some(25));
|
||||||
|
|
||||||
android_log(&format!("Downloading from: {}", download_url));
|
android_log(&format!("Downloading from: {}", download_url));
|
||||||
let response = client.get(download_url).send().await.map_err(|e| format!("Download error: {} (source: {:?})", e, StdError::source(&e)))?; android_log(&format!("Response status: {}", response.status()));
|
let response = client.get(&download_url).send().await.map_err(|e| format!("Download error: {} (source: {:?})", e, StdError::source(&e)))?;
|
||||||
|
android_log(&format!("Response status: {}", response.status()));
|
||||||
let total_size = response.content_length().unwrap_or(0) as usize;
|
let total_size = response.content_length().unwrap_or(0) as usize;
|
||||||
|
|
||||||
let temp_zip_path = base_dir.join("temp-payload.zip");
|
let temp_zip_path = base_dir.join("temp-payload.zip");
|
||||||
@@ -287,7 +550,7 @@ async fn download_and_extract_payload(app: tauri::AppHandle, window: WebviewWind
|
|||||||
downloaded += chunk.len();
|
downloaded += chunk.len();
|
||||||
|
|
||||||
let percent = if total_size > 0 {
|
let percent = if total_size > 0 {
|
||||||
10 + ((downloaded as f64 / total_size as f64) * 75.0) as u32
|
25 + ((downloaded as f64 / total_size as f64) * 65.0) as u32
|
||||||
} else {
|
} else {
|
||||||
35
|
35
|
||||||
};
|
};
|
||||||
@@ -310,7 +573,8 @@ async fn download_and_extract_payload(app: tauri::AppHandle, window: WebviewWind
|
|||||||
let zip_file = File::open(&zip_path_clone).map_err(|e| format!("Open zip error: {}", e))?;
|
let zip_file = File::open(&zip_path_clone).map_err(|e| format!("Open zip error: {}", e))?;
|
||||||
let mut archive = zip::ZipArchive::new(zip_file).map_err(|e| format!("Zip parse error: {}", e))?;
|
let mut archive = zip::ZipArchive::new(zip_file).map_err(|e| format!("Zip parse error: {}", e))?;
|
||||||
archive.extract(&extract_dir).map_err(|e| format!("Extraction error: {}", e))?;
|
archive.extract(&extract_dir).map_err(|e| format!("Extraction error: {}", e))?;
|
||||||
let _ = std::fs::remove_file(zip_path_clone);
|
let _ = std::fs::remove_file(&zip_path_clone);
|
||||||
|
let _ = std::fs::write(extract_dir.join(".installed"), "ok");
|
||||||
apply_patches(&extract_dir);
|
apply_patches(&extract_dir);
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -362,9 +626,37 @@ const ANDROID_VIDEO_FIX_SCRIPT: &str = r#"
|
|||||||
})();
|
})();
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
const EDGE_TO_EDGE_SCRIPT: &str = r#"
|
||||||
|
(function() {
|
||||||
|
var m = document.querySelector('meta[name=viewport]');
|
||||||
|
if (m && m.content.indexOf('viewport-fit') === -1) {
|
||||||
|
m.content += ', viewport-fit=cover';
|
||||||
|
}
|
||||||
|
function apply(t, r, b, l) {
|
||||||
|
var s = document.documentElement.style;
|
||||||
|
s.setProperty('--safe-top', t + 'px');
|
||||||
|
s.setProperty('--safe-right', r + 'px');
|
||||||
|
s.setProperty('--safe-bottom', b + 'px');
|
||||||
|
s.setProperty('--safe-left', l + 'px');
|
||||||
|
}
|
||||||
|
window.__vylaApplyInsets = apply;
|
||||||
|
try {
|
||||||
|
if (window.VylaInsets) {
|
||||||
|
var v = JSON.parse(window.VylaInsets.get());
|
||||||
|
apply(v.top, v.right, v.bottom, v.left);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
})();
|
||||||
|
"#;
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.on_page_load(|webview, payload| {
|
||||||
|
if payload.event() == tauri::webview::PageLoadEvent::Finished {
|
||||||
|
let _ = webview.eval(EDGE_TO_EDGE_SCRIPT);
|
||||||
|
}
|
||||||
|
})
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
@@ -433,25 +725,32 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(400));
|
std::thread::sleep(std::time::Duration::from_millis(600));
|
||||||
|
|
||||||
if let Some(window) = handle.get_webview_window("main") {
|
if let Some(window) = handle.get_webview_window("main") {
|
||||||
update_ui(&window, "Initializing Vyla Go engine...", Some(5));
|
update_ui(&window, "Initializing Vyla Go engine...", Some(5));
|
||||||
|
|
||||||
match download_and_extract_payload(handle.clone(), window.clone()).await {
|
loop {
|
||||||
Ok(_) => {
|
match download_and_extract_payload(handle.clone(), window.clone()).await {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(400));
|
Ok(_) => {
|
||||||
let _ = window.eval(&format!(
|
std::thread::sleep(std::time::Duration::from_millis(400));
|
||||||
"{}; window.location.replace('http://vyla.localhost/frontend/index.html');",
|
let _ = window.eval(&format!(
|
||||||
ANDROID_VIDEO_FIX_SCRIPT
|
"{}; window.location.replace('http://localhost:3001/');",
|
||||||
));
|
ANDROID_VIDEO_FIX_SCRIPT
|
||||||
}
|
));
|
||||||
Err(err) => {
|
break;
|
||||||
update_ui(
|
}
|
||||||
&window,
|
Err(err) => {
|
||||||
&format!("Error: {}", err),
|
android_log(&format!("Installer error: {}", err));
|
||||||
Some(0),
|
for remaining in (1..=8).rev() {
|
||||||
);
|
update_ui(
|
||||||
|
&window,
|
||||||
|
&format!("Error: {}. Retrying in {}s...", err, remaining),
|
||||||
|
Some(0),
|
||||||
|
);
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Vyla Go",
|
"productName": "Vyla Go",
|
||||||
"version": "1.0.3",
|
"version": "1.0.15",
|
||||||
"identifier": "com.vyla.go",
|
"identifier": "com.vyla.go",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "",
|
"beforeDevCommand": "",
|
||||||
|
|||||||
@@ -3,9 +3,17 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
<title>Vyla Go</title>
|
<title>Vyla Go</title>
|
||||||
<style>
|
<style>
|
||||||
|
:root {
|
||||||
|
--safe-top: env(safe-area-inset-top, 0px);
|
||||||
|
--safe-right: env(safe-area-inset-right, 0px);
|
||||||
|
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
--safe-left: env(safe-area-inset-left, 0px);
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -62,7 +70,7 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
padding: 20px 16px 30px 16px;
|
padding: 20px calc(16px + var(--safe-right)) calc(30px + var(--safe-bottom)) calc(16px + var(--safe-left));
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -122,7 +130,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="center-brand">
|
<div class="center-brand">
|
||||||
<img src="https://gitlab.com/vyla-entertainment/home-frontend/-/raw/main/assets/images/logo.png" alt="Logo" />
|
<img src="/white_logo.png" alt="Logo" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bottom-dock">
|
<div class="bottom-dock">
|
||||||
|
|||||||
BIN
ui/white_logo.png
Normal file
BIN
ui/white_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
Reference in New Issue
Block a user