chore: initial commit

This commit is contained in:
2026-09-08 16:47:20 -07:00
commit 3e5921fafa
104 changed files with 14099 additions and 0 deletions

43
.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
node_modules/
package-lock.json
dist/
dist-build/
temp-validation-extract/
update-payload.zip
manifest.json
validation-report.json
src-tauri/target/
src-tauri/gen/android/.gradle/
src-tauri/gen/android/app/build/
src-tauri/gen/android/build/
src-tauri/gen/android/local.properties
src-tauri/gen/android/.idea/
config/encrypted-credentials.json
credentials.json
.env
*.key
*.pem
*.jks
*.keystore
.DS_Store
Thumbs.db
desktop.ini
.vscode/
.idea/
*.swp
*.swo
*~
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*.tmp
*.temp
download-temp.zip

57
DOCUMENTATION/FORDEV.md Normal file
View File

@@ -0,0 +1,57 @@
Install dependencies:
```bash
npm install
```
Build the update payload:
```bash
npm run build:payload
```
Compile and assemble the release APK:
```bash
npm run build:apk
```
Release to GitLab:
```bash
npm run release
```
## Logs
Stream live device logs:
```powershell
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" logcat -s VylaGo
```
Clear the logcat buffer then stream:
```powershell
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" logcat -c; & "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" logcat -s VylaGo
```
Dump logs to file:
```powershell
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" logcat -d -s VylaGo > vyla-go.log
```
Show all crash/fatal logs:
```powershell
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" logcat -d | Select-String "VylaGo|AndroidRuntime|FATAL"
```
List connected devices:
```powershell
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" devices
```
Install APK directly to device:
```powershell
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" install -r "src-tauri\gen\android\app\build\outputs\apk\universal\release\app-universal-release.apk"
```
Force-stop and clear app data:
```powershell
& "$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
```

33
README.md Normal file
View File

@@ -0,0 +1,33 @@
# Vyla Go
Vyla Go is the native Android mobile launcher for the Vyla media ecosystem.
## Features
- **Native Android APK Architecture**: Built on Tauri v2 with native hardware acceleration.
- **Automated Resource Management**: Downloads, unpacks, and updates core services directly from official releases.
- **Hardware Shader Interface**: Real-time WebGL shader lighting and glassmorphism elements.
## Prerequisites
- Node.js 20+
- Rust & Cargo
- Android Studio with Android SDK & NDK 29+
- Toolchain targets:
```bash
rustup target add aarch64-linux-android
```
## Development & Build
Install dependencies:
```bash
npm install
```
Compile and assemble the release APK:
```bash
npm run build:apk
```
Release packages to GitLab:
```bash
npm run release
```

76
build-pipeline.js Normal file
View File

@@ -0,0 +1,76 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const AdmZip = require('adm-zip');
const { execSync } = require('child_process');
const BUILD_DIR = path.join(__dirname, 'dist-build');
const REPOS = ['frontend', 'player'];
const GITLAB_GROUP = 'vyla-entertainment';
const REPO_CONFIG = {
'frontend': { name: 'frontend', branch: 'main' },
'player': { name: 'player', branch: 'main' },
};
function cleanBuildDir() {
if (fs.existsSync(BUILD_DIR)) {
fs.rmSync(BUILD_DIR, { recursive: true, force: true });
}
fs.mkdirSync(BUILD_DIR, { recursive: true });
}
function clonePrivateRepo(repoConfig, targetDir) {
if (fs.existsSync(targetDir)) {
fs.rmSync(targetDir, { recursive: true, force: true });
}
const token = process.env.GITLAB_TOKEN || process.env.VYLA_GITLAB_TOKEN;
const auth = token ? `oauth2:${token}@` : '';
const repoUrl = `https://${auth}gitlab.com/${GITLAB_GROUP}/${repoConfig.name}.git`;
execSync(`git clone --depth 1 --branch ${repoConfig.branch} ${repoUrl} ${targetDir}`, { stdio: 'inherit' });
}
async function build() {
cleanBuildDir();
for (const [key, config] of Object.entries(REPO_CONFIG)) {
const targetDir = path.join(__dirname, key);
clonePrivateRepo(config, targetDir);
execSync('npm install --omit=dev', { cwd: targetDir, stdio: 'inherit' });
}
const zip = new AdmZip();
const configDir = path.join(__dirname, 'config');
if (fs.existsSync(configDir)) {
zip.addLocalFolder(configDir, 'config');
}
for (const repo of REPOS) {
const srcRepo = path.join(__dirname, repo);
zip.addLocalFolder(srcRepo, repo);
}
const zipPath = path.join(__dirname, 'update-payload.zip');
if (fs.existsSync(zipPath)) fs.rmSync(zipPath, { force: true });
zip.writeZip(zipPath);
const hash = crypto.createHash('sha256');
hash.update(fs.readFileSync(zipPath));
const checksum = hash.digest('hex');
const manifest = {
version: JSON.parse(fs.readFileSync('package.json', 'utf8')).version,
sha256: checksum,
timestamp: new Date().toISOString()
};
fs.writeFileSync(path.join(__dirname, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
for (const repo of REPOS) {
fs.rmSync(path.join(__dirname, repo), { recursive: true, force: true });
}
}
build().catch(err => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,5 @@
{
"player": {
"TMDB_KEY": "3f6703f3e2a0b2b40fe4fbb3e1546244"
}
}

5
config/env.json Normal file
View File

@@ -0,0 +1,5 @@
{
"player": {
"PORT": "3000"
}
}

18
config/replacements.json Normal file
View File

@@ -0,0 +1,18 @@
{
"frontend": [
{
"find": "https://player.vyla.cc",
"replace": "http://127.0.0.1:3000"
},
{
"find": "http://player.vyla.cc",
"replace": "http://127.0.0.1:3000"
}
],
"player": [
{
"find": "https://player.vyla.cc",
"replace": "http://127.0.0.1:3000"
}
]
}

109
credential-manager.js Normal file
View File

@@ -0,0 +1,109 @@
const fs = require('fs');
const path = require('path');
const CryptoUtils = require('./crypto-utils');
class CredentialManager {
constructor(appDataPath) {
this.appDataPath = appDataPath;
this.credentialsPath = path.join(appDataPath, 'vyla-credentials-store.json');
this.machineKey = CryptoUtils.getMachineKey(appDataPath);
this.encryptionKey = CryptoUtils.deriveKey(this.machineKey);
this.credentials = {};
}
loadCredentials() {
try {
if (fs.existsSync(this.credentialsPath)) {
const encryptedData = fs.readFileSync(this.credentialsPath, 'utf8');
this.credentials = CryptoUtils.decryptObject(encryptedData, this.encryptionKey);
} else {
this.credentials = {};
}
} catch (error) {
this.credentials = {};
}
}
saveCredentials() {
try {
const encryptedData = CryptoUtils.encryptObject(this.credentials, this.encryptionKey);
fs.writeFileSync(this.credentialsPath, encryptedData, 'utf8');
} catch (error) {
throw error;
}
}
getCredential(service, key) {
if (!this.credentials[service]) {
return null;
}
return this.credentials[service][key] || null;
}
setCredential(service, key, value) {
if (!this.credentials[service]) {
this.credentials[service] = {};
}
this.credentials[service][key] = value;
this.saveCredentials();
}
getServiceCredentials(service) {
return this.credentials[service] || {};
}
setServiceCredentials(service, credentials) {
this.credentials[service] = { ...credentials };
this.saveCredentials();
}
hasServiceCredentials(service) {
return !!this.credentials[service] && Object.keys(this.credentials[service]).length > 0;
}
deleteServiceCredentials(service) {
delete this.credentials[service];
this.saveCredentials();
}
initializeFromConfig(config) {
let initialized = false;
for (const [service, keys] of Object.entries(config)) {
if (!this.hasServiceCredentials(service)) {
this.setServiceCredentials(service, keys);
initialized = true;
}
}
return initialized;
}
getAllEnvVars() {
const envVars = {};
for (const [service, credentials] of Object.entries(this.credentials)) {
envVars[service] = { ...credentials };
}
return envVars;
}
verifyIntegrity() {
try {
if (!fs.existsSync(this.credentialsPath)) {
return false;
}
const encryptedData = fs.readFileSync(this.credentialsPath, 'utf8');
CryptoUtils.decryptObject(encryptedData, this.encryptionKey);
return true;
} catch (error) {
return false;
}
}
resetCredentials() {
this.credentials = {};
if (fs.existsSync(this.credentialsPath)) {
fs.unlinkSync(this.credentialsPath);
}
}
}
module.exports = CredentialManager;

146
crypto-utils.js Normal file
View File

@@ -0,0 +1,146 @@
const crypto = require('crypto');
const os = require('os');
const { execSync } = require('child_process');
class CryptoUtils {
static getMachineKey(cacheDir) {
const path = require('path');
const fs = require('fs');
const cachePath = cacheDir ? path.join(cacheDir, '.machine-key-cache') : null;
if (cachePath && fs.existsSync(cachePath)) {
try {
const cached = fs.readFileSync(cachePath, 'utf8').trim();
if (cached) return cached;
} catch (e) {
}
}
let key;
try {
const machineId = [
os.hostname(),
os.platform(),
os.arch(),
os.cpus()[0].model,
this.getMachineId(),
process.env.COMPUTERNAME || '',
process.env.USERNAME || ''
].join('|');
const hash = crypto.createHash('sha256');
hash.update(machineId);
key = hash.digest('hex');
} catch (error) {
const fallback = crypto.createHash('sha256');
fallback.update(os.hostname() + Date.now());
key = fallback.digest('hex');
}
if (cachePath) {
try {
fs.writeFileSync(cachePath, key, 'utf8');
} catch (e) {
}
}
return key;
}
static getMachineId() {
const fs = require('fs');
if (os.platform() === 'win32') {
try {
const { spawnSync } = require('child_process');
const res = spawnSync('reg', ['query', 'HKLM\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid'], { encoding: 'utf8' });
if (res.stdout) {
const match = res.stdout.match(/MachineGuid\s+REG_SZ\s+([^\r\n]+)/);
if (match) return match[1].trim();
}
} catch (e) { }
return 'windows-node-' + os.hostname();
} else if (os.platform() === 'darwin') {
try {
const { spawnSync } = require('child_process');
const res = spawnSync('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice'], { encoding: 'utf8' });
const match = res.stdout.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
if (match) return match[1].trim();
} catch (e) { }
return 'darwin-node-' + os.hostname();
} else {
for (const idPath of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {
try {
if (fs.existsSync(idPath)) return fs.readFileSync(idPath, 'utf8').trim();
} catch (e) { }
}
return 'linux-node-' + os.hostname();
}
}
static deriveKey(machineKey, salt = 'vyla-home-salt') {
const hash = crypto.createHash('sha256');
hash.update(machineKey + salt);
return hash.digest();
}
static encrypt(plaintext, key) {
try {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
} catch (error) {
throw new Error(`Encryption failed: ${error.message}`);
}
}
static decrypt(encryptedData, key) {
try {
const parts = encryptedData.split(':');
if (parts.length !== 3) {
throw new Error('Invalid encrypted data format');
}
const iv = Buffer.from(parts[0], 'hex');
const authTag = Buffer.from(parts[1], 'hex');
const encrypted = parts[2];
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
throw new Error(`Decryption failed: ${error.message}`);
}
}
static encryptObject(obj, key) {
const json = JSON.stringify(obj);
return this.encrypt(json, key);
}
static decryptObject(encryptedData, key) {
const json = this.decrypt(encryptedData, key);
return JSON.parse(json);
}
static hashData(data) {
const hash = crypto.createHash('sha256');
hash.update(data);
return hash.digest('hex');
}
static verifyHash(data, expectedHash) {
const actualHash = this.hashData(data);
return actualHash === expectedHash;
}
}
module.exports = CryptoUtils;

25
package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "vyla-go",
"version": "1.0.0",
"description": "Vyla Go Mobile Launcher",
"main": "main.js",
"scripts": {
"tauri": "tauri",
"dev": "tauri android dev",
"build:payload": "node build-pipeline.js",
"build:apk": "tauri android build --apk --target aarch64 --target x86_64",
"release": "node release.js --apk"
},
"dependencies": {
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-opener": "^2.0.0",
"@tauri-apps/plugin-process": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.0.0",
"@vyla-entertainment/sdk": "^1.11.0",
"adm-zip": "^0.6.0",
"express": "^4.19.2"
},
"devDependencies": {
"@tauri-apps/cli": "^2.0.0"
}
}

187
patcher.js Normal file
View File

@@ -0,0 +1,187 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const AdmZip = require('adm-zip');
const CryptoUtils = require('./crypto-utils');
class Patcher {
constructor(appDataPath) {
this.appDataPath = appDataPath;
}
getCurrentVersion() {
const versionFile = path.join(this.appDataPath, 'version.json');
if (fs.existsSync(versionFile)) {
try {
return JSON.parse(fs.readFileSync(versionFile, 'utf8')).version || '0.0.0';
} catch (e) {
return '0.0.0';
}
}
return '0.0.0';
}
getCurrentSha() {
const versionFile = path.join(this.appDataPath, 'version.json');
if (fs.existsSync(versionFile)) {
try {
return JSON.parse(fs.readFileSync(versionFile, 'utf8')).sha256 || null;
} catch (e) {
return null;
}
}
return null;
}
bridgeCredentials(credentialManager) {
const encryptedPath = path.join(this.appDataPath, 'encrypted-credentials.json');
if (!fs.existsSync(encryptedPath)) {
return false;
}
const buildKey = CryptoUtils.deriveKey('vyla-home-build-key', 'vyla-home-payload-salt-v1');
const encryptedData = fs.readFileSync(encryptedPath, 'utf8');
const credentials = CryptoUtils.decryptObject(encryptedData, buildKey);
for (const [service, keys] of Object.entries(credentials)) {
credentialManager.setServiceCredentials(service, keys);
}
fs.unlinkSync(encryptedPath);
return true;
}
saveCurrentVersion(version, sha256 = null) {
const versionFile = path.join(this.appDataPath, 'version.json');
fs.mkdirSync(this.appDataPath, { recursive: true });
fs.writeFileSync(versionFile, JSON.stringify({ version, sha256 }, null, 2), 'utf8');
}
purgeOldCode() {
const dirs = ['frontend', 'player', 'stream-api', 'auth-proxy', 'config'];
for (const dir of dirs) {
const fullPath = path.join(this.appDataPath, dir);
if (fs.existsSync(fullPath)) {
try {
const trashPath = path.join(this.appDataPath, `.trash_${dir}_${Date.now()}`);
fs.renameSync(fullPath, trashPath);
fs.rmSync(trashPath, { recursive: true, force: true });
} catch (e) {
try {
fs.rmSync(fullPath, { recursive: true, force: true });
} catch (err2) { }
}
}
}
try {
const files = fs.readdirSync(this.appDataPath);
for (const file of files) {
if (file.startsWith('.trash_')) {
fs.rmSync(path.join(this.appDataPath, file), { recursive: true, force: true });
}
}
} catch (e) { }
}
extractZipPayload(zipPath) {
const AdmZip = require('adm-zip');
const zip = new AdmZip(zipPath);
const entries = zip.getEntries();
if (!entries || entries.length === 0) {
throw new Error('Downloaded archive is empty, invalid, or truncated.');
}
zip.extractAllTo(this.appDataPath, true);
const testPath = path.join(this.appDataPath, 'frontend', 'index.html');
if (!fs.existsSync(testPath)) {
const { execSync } = require('child_process');
if (process.platform === 'win32') {
execSync(`powershell.exe -NoProfile -NonInteractive -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${this.appDataPath}' -Force"`, { windowsHide: true });
} else {
execSync(`unzip -o "${zipPath}" -d "${this.appDataPath}"`);
}
}
}
verifyChecksum(zipPath, expectedSha256) {
if (!expectedSha256) return true;
const hash = crypto.createHash('sha256');
const data = fs.readFileSync(zipPath);
hash.update(data);
const calculated = hash.digest('hex');
return calculated === expectedSha256;
}
verifyExtraction() {
const requiredFiles = [
'frontend/index.html',
'player/server.js'
];
for (const file of requiredFiles) {
const fullPath = path.join(this.appDataPath, file);
if (!fs.existsSync(fullPath)) {
return false;
}
const stat = fs.statSync(fullPath);
if (stat.size === 0) {
return false;
}
try {
const sampleSize = Math.min(stat.size, 4096);
const fd = fs.openSync(fullPath, 'r');
const buffer = Buffer.alloc(sampleSize);
fs.readSync(fd, buffer, 0, sampleSize, 0);
fs.closeSync(fd);
if (buffer.every(byte => byte === 0)) {
return false;
}
} catch (e) {
return false;
}
}
return true;
}
processDirectory(dir, replacementList = []) {
const files = fs.readdirSync(dir);
for (const file of files) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
if (file === 'node_modules' || file === '.git') continue;
this.processDirectory(fullPath, replacementList);
} else if (stat.isFile()) {
const ext = path.extname(fullPath).toLowerCase();
let content = fs.readFileSync(fullPath, 'utf8');
let modified = false;
for (const rep of replacementList) {
if (content.includes(rep.find)) {
content = content.split(rep.find).join(rep.replace);
modified = true;
}
}
if (modified) {
fs.writeFileSync(fullPath, content, 'utf8');
}
}
}
}
applyPatches(replacements) {
const targetDirs = ['frontend', 'player'];
for (const dirName of targetDirs) {
const dirPath = path.join(this.appDataPath, dirName);
if (!fs.existsSync(dirPath)) continue;
const replacementList = replacements[dirName] || [];
this.processDirectory(dirPath, replacementList);
}
}
}
module.exports = Patcher;

189
release.js Normal file
View File

@@ -0,0 +1,189 @@
const fs = require('fs');
const path = require('path');
const https = require('https');
const { execSync } = require('child_process');
const PROJECT_ID = process.env.CI_PROJECT_ID || 'vyla-entertainment%2Fgo';
const TOKEN = process.env.GITLAB_TOKEN || process.env.VYLA_GITLAB_TOKEN;
if (!TOKEN) {
console.error('Error: GITLAB_TOKEN or VYLA_GITLAB_TOKEN is required.');
process.exit(1);
}
function incrementVersion() {
const pkgPath = path.join(__dirname, 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
let [major, minor, patch] = (pkg.version || '1.0.0').split('.').map(Number);
patch = (patch || 0) + 1;
const newVersion = `${major}.${minor}.${patch}`;
pkg.version = newVersion;
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
const tauriPath = path.join(__dirname, 'src-tauri', 'tauri.conf.json');
if (fs.existsSync(tauriPath)) {
const tauriConf = JSON.parse(fs.readFileSync(tauriPath, 'utf8'));
tauriConf.version = newVersion;
fs.writeFileSync(tauriPath, JSON.stringify(tauriConf, null, 2) + '\n', 'utf8');
}
return newVersion;
}
function gitCommitAndTag(version) {
try {
execSync('git add package.json src-tauri/tauri.conf.json', { stdio: 'inherit' });
execSync(`git commit -m "chore: release ${version}"`, { stdio: 'inherit' });
execSync(`git tag -a ${version} -m "Release ${version}"`, { stdio: 'inherit' });
execSync('git push origin HEAD --tags', { stdio: 'inherit' });
} catch (e) {
console.warn('Git operations failed (non-fatal):', e.message);
}
}
function httpsRequest(options, body = null) {
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) resolve({ status: res.statusCode, body: data });
else reject(new Error(`HTTP ${res.statusCode}: ${data}`));
});
});
req.on('error', reject);
if (body) req.write(body);
req.end();
});
}
function uploadFile(filePath, packageName, version, fileName) {
return new Promise((resolve, reject) => {
const stat = fs.statSync(filePath);
const timestamp = Date.now();
const uploadName = `${path.parse(fileName).name}-${timestamp}${path.parse(fileName).ext}`;
const urlPath = `/api/v4/projects/${PROJECT_ID}/packages/generic/${packageName}/${version}/${encodeURIComponent(uploadName)}`;
const req = https.request({
hostname: 'gitlab.com',
path: urlPath,
method: 'PUT',
headers: {
'PRIVATE-TOKEN': TOKEN,
'Content-Type': 'application/octet-stream',
'Content-Length': stat.size
}
}, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve({ uploadName, url: `https://gitlab.com/api/v4/projects/${PROJECT_ID}/packages/generic/${packageName}/${version}/${encodeURIComponent(uploadName)}` });
} else {
reject(new Error(`Upload failed (${res.statusCode}): ${body}`));
}
});
});
req.on('error', reject);
fs.createReadStream(filePath).pipe(req);
});
}
async function createRelease(version, assets) {
const tag = version;
const apk = assets.apk;
const payload = assets.payload;
const manifest = assets.manifest;
const description = [
`## Automated Release ${version}`,
'',
'### Assets',
'',
`* [vyla-go-${version}.apk](${apk.url})`,
'',
'### Payload',
'',
`* [manifest.json](${manifest.url})`,
`* [update-payload.zip](${payload.url})`,
].join('\n');
const body = JSON.stringify({
name: `Vyla Go Release ${version}`,
tag_name: tag,
description,
assets: {
links: [
{ name: `vyla-go-${version}.apk`, url: apk.url, link_type: 'package' },
{ name: 'update-payload.zip', url: payload.url, link_type: 'other' },
{ name: 'manifest.json', url: manifest.url, link_type: 'other' },
]
}
});
await httpsRequest({
hostname: 'gitlab.com',
path: `/api/v4/projects/${PROJECT_ID}/releases`,
method: 'POST',
headers: {
'PRIVATE-TOKEN': TOKEN,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body)
}
}, body);
}
async function main() {
const rawVersion = incrementVersion();
const version = `v${rawVersion}`;
console.log(`Releasing ${version}...`);
console.log('Building payload...');
execSync('npm run build:payload', { stdio: 'inherit' });
console.log('Building APK...');
execSync('npm run build:apk', { stdio: 'inherit' });
const apkSearchDirs = [
path.join(__dirname, 'src-tauri', 'gen', 'android', 'app', 'build', 'outputs', 'apk', 'universal', 'release'),
path.join(__dirname, 'src-tauri', 'gen', 'android', 'app', 'build', 'outputs', 'apk', 'arm64', 'release'),
path.join(__dirname, 'src-tauri', 'gen', 'android', 'app', 'build', 'outputs', 'apk', 'release'),
];
let apkPath = null;
for (const dir of apkSearchDirs) {
if (!fs.existsSync(dir)) continue;
const found = fs.readdirSync(dir).find(f => f.endsWith('.apk') && !f.includes('unsigned'));
if (found) { apkPath = path.join(dir, found); break; }
}
if (!apkPath) throw new Error('Signed release APK not found');
const payloadPath = path.join(__dirname, 'update-payload.zip');
const manifestPath = path.join(__dirname, 'manifest.json');
if (!fs.existsSync(payloadPath)) throw new Error('update-payload.zip not found — run build:payload first');
if (!fs.existsSync(manifestPath)) throw new Error('manifest.json not found — run build:payload first');
gitCommitAndTag(version);
console.log('Uploading APK...');
const apkAsset = await uploadFile(apkPath, 'vyla-go', version, `vyla-go-${version}.apk`);
console.log('Uploading payload...');
const payloadAsset = await uploadFile(payloadPath, 'vyla-go', version, 'update-payload.zip');
console.log('Uploading manifest...');
const manifestAsset = await uploadFile(manifestPath, 'vyla-go', version, 'manifest.json');
console.log('Creating GitLab release...');
await createRelease(version, { apk: apkAsset, payload: payloadAsset, manifest: manifestAsset });
console.log(`\nRelease ${version} published successfully.`);
console.log(`APK: ${apkAsset.url}`);
console.log(`Payload: ${payloadAsset.url}`);
console.log(`Manifest: ${manifestAsset.url}`);
}
main().catch(err => {
console.error(err);
process.exit(1);
});

200
service-manager.js Normal file
View File

@@ -0,0 +1,200 @@
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const CredentialManager = require('./credential-manager');
class ServiceManager {
constructor(appDataPath, envConfig) {
this.appDataPath = appDataPath;
this.envConfig = envConfig;
this.credentialManager = new CredentialManager(appDataPath);
this.processes = {};
this.restartCounts = {};
this.stableTimers = {};
this.isStopping = false;
this.logsDir = path.join(appDataPath, 'logs');
fs.mkdirSync(this.logsDir, { recursive: true });
}
logToFile(name, line) {
try {
const logPath = path.join(this.logsDir, `${name}.log`);
const timestamp = new Date().toISOString();
fs.appendFile(logPath, `[${timestamp}] ${line}\n`, 'utf8', () => { });
process.stdout.write(JSON.stringify({ event: "service-log", data: { service: name, message: line } }) + '\n');
} catch (e) {
}
}
async startService(name, relativeDir, startScript = 'server.js') {
if (this.processes[name]) {
return;
}
const serviceDir = path.join(this.appDataPath, relativeDir);
const scriptPath = path.join(serviceDir, startScript);
if (!fs.existsSync(scriptPath)) {
throw new Error(`Service script not found: ${scriptPath}`);
}
let tokenSecret = process.env.TOKEN_SECRET ||
this.credentialManager.getCredential('player', 'TOKEN_SECRET');
if (!tokenSecret || tokenSecret.length < 32) {
tokenSecret = require('crypto').randomBytes(32).toString('hex');
this.credentialManager.setCredential('player', 'TOKEN_SECRET', tokenSecret);
}
const env = {
...process.env,
TOKEN_SECRET: tokenSecret,
...(this.envConfig[name] || {}),
...(this.credentialManager.getServiceCredentials(name) || {})
};
this.logToFile(name, `--- Starting ${name} in ${serviceDir} ---`);
const nodeExecutable = process.env.NODE_EXE || 'node';
const child = spawn(nodeExecutable, [scriptPath], {
cwd: serviceDir,
env,
stdio: ['ignore', 'pipe', 'pipe'],
shell: false
});
this.processes[name] = {
child,
relativeDir,
startScript,
status: 'starting'
};
if (this.stableTimers[name]) {
clearTimeout(this.stableTimers[name]);
}
this.stableTimers[name] = setTimeout(() => {
this.restartCounts[name] = 0;
}, 60000);
child.stdout.on('data', (data) => {
const text = data.toString().trim();
this.logToFile(name, `STDOUT: ${text}`);
});
child.stderr.on('data', (data) => {
const text = data.toString().trim();
this.logToFile(name, `STDERR: ${text}`);
if (text.includes('EADDRINUSE')) {
const match = text.match(/:(\d+)/);
const port = match ? match[1] : null;
if (port) {
try {
if (process.platform === 'win32') {
require('child_process').spawnSync('powershell.exe', [
'-NoProfile',
'-NonInteractive',
'-Command',
`Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }`
], { stdio: 'ignore', windowsHide: true });
} else {
require('child_process').spawnSync('fuser', ['-k', '-9', `${port}/tcp`], { stdio: 'ignore' });
}
} catch (_) { }
}
}
});
child.on('exit', (code, signal) => {
this.logToFile(name, `EXITED: code=${code} signal=${signal}`);
this.processes[name] = null;
if (this.stableTimers[name]) {
clearTimeout(this.stableTimers[name]);
this.stableTimers[name] = null;
}
if (!this.isStopping) {
this.restartCounts[name] = (this.restartCounts[name] || 0) + 1;
if (this.restartCounts[name] > 5) {
this.logToFile(name, `Crashed too many times, giving up.`);
return;
}
setTimeout(() => {
this.startService(name, relativeDir, startScript).catch(err => {
this.logToFile(name, `Failed to restart: ${err.message}`);
});
}, 1500);
}
});
}
async startServices(services) {
await Promise.all(services.map(({ name, relativeDir, startScript }) =>
this.startService(name, relativeDir, startScript)
));
}
async stopAll() {
this.isStopping = true;
for (const name of Object.keys(this.stableTimers)) {
if (this.stableTimers[name]) {
clearTimeout(this.stableTimers[name]);
}
}
this.stableTimers = {};
const killPromises = Object.keys(this.processes).map((name) => {
const procInfo = this.processes[name];
if (procInfo && procInfo.child) {
this.logToFile(name, `Killing service`);
return new Promise((resolve) => {
procInfo.child.removeAllListeners('exit');
procInfo.child.on('exit', () => resolve());
procInfo.child.kill();
});
}
return Promise.resolve();
});
await Promise.all(killPromises);
this.processes = {};
}
async waitForPorts(ports, timeoutMs = 30000) {
await Promise.all(ports.map((port) => this.waitForPort(port, timeoutMs)));
}
async waitForPort(port, timeoutMs = 30000) {
const start = Date.now();
const net = require('net');
while (Date.now() - start < timeoutMs) {
if (this.isStopping) return false;
const connected = await new Promise((resolve) => {
const socket = new net.Socket();
socket.setTimeout(1000);
socket.on('connect', () => {
socket.destroy();
resolve(true);
});
socket.on('error', () => {
resolve(false);
});
socket.on('timeout', () => {
socket.destroy();
resolve(false);
});
socket.connect(port, '127.0.0.1');
});
if (connected) return true;
await new Promise(r => setTimeout(r, 40));
}
throw new Error(`Timeout waiting for port ${port}`);
}
}
module.exports = ServiceManager;

5540
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

25
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,25 @@
[package]
name = "vyla-go"
version = "1.0.0"
description = "Vyla Go Native Shell"
authors = ["Vyla Entertainment"]
edition = "2021"
[lib]
name = "vyla_go_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.1", features = [] }
[dependencies]
tauri = { version = "2.1", features = [] }
tauri-plugin-opener = "2"
tauri-plugin-process = "2"
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json", "stream"] }
zip = { version = "2.2", default-features = false, features = ["deflate"] }
futures-util = "0.3"
urlencoding = "2.1"

7
src-tauri/bin/node.deb Normal file
View File

@@ -0,0 +1,7 @@
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>

34
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,34 @@
fn main() {
let target = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target == "android" {
let manifest_dir = std::path::PathBuf::from(
std::env::var("CARGO_MANIFEST_DIR").unwrap()
);
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
let abi = match target_arch.as_str() {
"aarch64" => "arm64-v8a",
"x86_64" => "x86_64",
"x86" => "x86",
"arm" => "armeabi-v7a",
_ => "arm64-v8a",
};
let jni_libs = manifest_dir
.join("gen")
.join("android")
.join("app")
.join("src")
.join("main")
.join("jniLibs")
.join(abi);
println!("cargo:warning=libnode search path: {}", jni_libs.display());
println!("cargo:warning=libnode exists: {}", jni_libs.join("libnode.so").exists());
println!("cargo:rustc-link-search=native={}", jni_libs.display());
println!("cargo:rustc-link-lib=dylib=node");
}
tauri_build::build();
}

15
src-tauri/capabilities Normal file
View File

@@ -0,0 +1,15 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default permissions for Vyla Go",
"windows": ["main"],
"permissions": [
"core:default",
"core:event:default",
"core:event:allow-listen",
"core:event:allow-emit",
"opener:default",
"process:default",
"shell:default"
]
}

View File

@@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

20
src-tauri/gen/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
build
/captures
.externalNativeBuild
.cxx
local.properties
key.properties
keystore.properties
/.tauri
/tauri.settings.gradle

6
src-tauri/gen/android/app/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/src/main/**/generated
/src/main/jniLibs/**/*.so
/src/main/assets/tauri.conf.json
/tauri.build.gradle.kts
/proguard-tauri.pro
/tauri.properties

View File

@@ -0,0 +1,79 @@
import java.util.Properties
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("rust")
}
val tauriProperties = Properties().apply {
val propFile = file("tauri.properties")
if (propFile.exists()) {
propFile.inputStream().use { load(it) }
}
}
android {
compileSdk = 36
namespace = "com.vyla.go"
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "com.vyla.go"
minSdk = 24
targetSdk = 36
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
}
signingConfigs {
create("release") {
storeFile = file("${project.rootDir}/vyla-release.jks")
storePassword = "vylapass"
keyAlias = "vyla"
keyPassword = "vylapass"
enableV1Signing = true
enableV2Signing = true
}
}
buildTypes {
getByName("release") {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
getByName("debug") {
signingConfig = signingConfigs.getByName("release")
}
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
buildConfig = true
}
packaging {
jniLibs {
useLegacyPackaging = true
}
}
}
rust {
rootDirRel = "../../../"
}
dependencies {
implementation("androidx.webkit:webkit:1.14.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.activity:activity-ktx:1.10.1")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.lifecycle:lifecycle-process:2.10.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}
apply(from = "tauri.build.gradle.kts")

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config"
android:largeHeap="true"
android:extractNativeLibs="true"
android:theme="@style/Theme.vyla_go">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:launchMode="singleTask"
android:label="@string/main_activity_title"
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,9 @@
package com.vyla.go
import android.os.Bundle
class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.vyla_go" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

View File

@@ -0,0 +1,4 @@
<resources>
<string name="app_name">"Vyla Go"</string>
<string name="main_activity_title">"Vyla Go"</string>
</resources>

View File

@@ -0,0 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.vyla_go" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>

View File

@@ -0,0 +1,22 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.11.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
tasks.register("clean").configure {
delete("build")
}

View File

@@ -0,0 +1,23 @@
plugins {
`kotlin-dsl`
}
gradlePlugin {
plugins {
create("pluginsForCoolKids") {
id = "rust"
implementationClass = "RustPlugin"
}
}
}
repositories {
google()
mavenCentral()
}
dependencies {
compileOnly(gradleApi())
implementation("com.android.tools.build:gradle:8.11.0")
}

View File

@@ -0,0 +1,68 @@
import java.io.File
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
open class BuildTask : DefaultTask() {
@Input
var rootDirRel: String? = null
@Input
var target: String? = null
@Input
var release: Boolean? = null
@TaskAction
fun assemble() {
val executable = """npm""";
try {
runTauriCli(executable)
} catch (e: Exception) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
// Try different Windows-specific extensions
val fallbacks = listOf(
"$executable.exe",
"$executable.cmd",
"$executable.bat",
)
var lastException: Exception = e
for (fallback in fallbacks) {
try {
runTauriCli(fallback)
return
} catch (fallbackException: Exception) {
lastException = fallbackException
}
}
throw lastException
} else {
throw e;
}
}
}
fun runTauriCli(executable: String) {
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
val target = target ?: throw GradleException("target cannot be null")
val release = release ?: throw GradleException("release cannot be null")
val args = listOf("run", "--", "tauri", "android", "android-studio-script");
project.exec {
workingDir(File(project.projectDir, rootDirRel))
executable(executable)
args(args)
if (project.logger.isEnabled(LogLevel.DEBUG)) {
args("-vv")
} else if (project.logger.isEnabled(LogLevel.INFO)) {
args("-v")
}
if (release) {
args("--release")
}
args(listOf("--target", target))
}.assertNormalExitValue()
}
}

View File

@@ -0,0 +1,85 @@
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.get
const val TASK_GROUP = "rust"
open class Config {
lateinit var rootDirRel: String
}
open class RustPlugin : Plugin<Project> {
private lateinit var config: Config
override fun apply(project: Project) = with(project) {
config = extensions.create("rust", Config::class.java)
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
extensions.configure<ApplicationExtension> {
@Suppress("UnstableApiUsage")
flavorDimensions.add("abi")
productFlavors {
create("universal") {
dimension = "abi"
ndk {
abiFilters += abiList
}
}
defaultArchList.forEachIndexed { index, arch ->
create(arch) {
dimension = "abi"
ndk {
abiFilters.add(defaultAbiList[index])
}
}
}
}
}
afterEvaluate {
for (profile in listOf("debug", "release")) {
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
val buildTask = tasks.maybeCreate(
"rustBuildUniversal$profileCapitalized",
DefaultTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for all targets"
}
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
for (targetPair in targetsList.withIndex()) {
val targetName = targetPair.value
val targetArch = archList[targetPair.index]
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
val targetBuildTask = project.tasks.maybeCreate(
"rustBuild$targetArchCapitalized$profileCapitalized",
BuildTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for $targetArch"
rootDirRel = config.rootDirRel
target = targetName
release = profile == "release"
}
buildTask.dependsOn(targetBuildTask)
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
targetBuildTask
)
}
}
}
}
}

View File

@@ -0,0 +1,24 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonFinalResIds=false

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Tue May 10 19:22:52 CST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

185
src-tauri/gen/android/gradlew vendored Normal file
View File

@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
src-tauri/gen/android/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,3 @@
include ':app'
apply from: 'tauri.settings.gradle'

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
{}

File diff suppressed because it is too large Load Diff

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Some files were not shown because too many files have changed in this diff Show More