Added support for downloading assets (including auto-update)
- Added support for downloading assets (including auto-update) - By default, auto-update is turned off. Enable it in settings. - Default repository for app icons and names is [https://github.com/tverona1/QuestAppLauncher_assets]. This can be configured in config.json.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6269b6df21455a244a60057eaa3ff7af
|
||||
guid: 6da7b442ff875544cbb8aff0c6ca585a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 577d9725f58264943855b8ac185531fe
|
||||
folderAsset: yes
|
||||
timeCreated: 1466788344
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 14f21d7a1e53a8c4e87b25526a7eb63c
|
||||
folderAsset: yes
|
||||
timeCreated: 1466788345
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aadad8ac54f29e44583510294ac5c312
|
||||
timeCreated: 1466788355
|
||||
licenseType: Store
|
||||
TextScriptImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,76 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a3c684705042f345975d924f6983e36
|
||||
timeCreated: 1466788352
|
||||
licenseType: Store
|
||||
PluginImporter:
|
||||
serializedVersion: 1
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
isPreloaded: 0
|
||||
platformData:
|
||||
Android:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
Any:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
Editor:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
DefaultValueInitialized: true
|
||||
OS: AnyOS
|
||||
Linux:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: x86
|
||||
Linux64:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: x86_64
|
||||
OSXIntel:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
OSXIntel64:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
SamsungTV:
|
||||
enabled: 1
|
||||
settings:
|
||||
STV_MODEL: STANDARD_13
|
||||
Tizen:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
WebGL:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
Win:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
Win64:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
WindowsStoreApps:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
DontProcess: False
|
||||
PlaceholderPath: Assets/JsonDotNet/Assemblies/Standalone/Newtonsoft.Json.dll
|
||||
SDK: AnySDK
|
||||
ScriptingBackend: Il2Cpp
|
||||
iOS:
|
||||
enabled: 1
|
||||
settings:
|
||||
CompileFlags:
|
||||
FrameworkDependencies:
|
||||
tvOS:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 01ef782d02bb1994dbe418b69432552b
|
||||
folderAsset: yes
|
||||
timeCreated: 1466788344
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6807fedb8dcaf04682d2c84f0ab753f
|
||||
timeCreated: 1466788355
|
||||
licenseType: Store
|
||||
TextScriptImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,75 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 17aef65a15b471f468b5fbeb4ff0c6a1
|
||||
timeCreated: 1466788349
|
||||
licenseType: Store
|
||||
PluginImporter:
|
||||
serializedVersion: 1
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
isPreloaded: 0
|
||||
platformData:
|
||||
Android:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
Any:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
Editor:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
DefaultValueInitialized: true
|
||||
OS: AnyOS
|
||||
Linux:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: x86
|
||||
Linux64:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: x86_64
|
||||
LinuxUniversal:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
OSXIntel:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
OSXIntel64:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
OSXUniversal:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
SamsungTV:
|
||||
enabled: 0
|
||||
settings:
|
||||
STV_MODEL: STANDARD_13
|
||||
Win:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
Win64:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
WindowsStoreApps:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
DontProcess: False
|
||||
PlaceholderPath:
|
||||
SDK: AnySDK
|
||||
ScriptingBackend: Il2Cpp
|
||||
iOS:
|
||||
enabled: 0
|
||||
settings:
|
||||
CompileFlags:
|
||||
FrameworkDependencies:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1418141139a6ac443b18cb05c0643a29
|
||||
folderAsset: yes
|
||||
timeCreated: 1466788345
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36f7323c55864364d8bb88c736e4bca6
|
||||
timeCreated: 1466788355
|
||||
licenseType: Store
|
||||
TextScriptImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,67 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b6ba260dada0ea4a871a42011f8b87d
|
||||
timeCreated: 1466788355
|
||||
licenseType: Store
|
||||
PluginImporter:
|
||||
serializedVersion: 1
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
isPreloaded: 0
|
||||
platformData:
|
||||
Android:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
Any:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
Editor:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
DefaultValueInitialized: true
|
||||
OS: AnyOS
|
||||
Linux:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: x86
|
||||
Linux64:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: x86_64
|
||||
OSXIntel:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
OSXIntel64:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
SamsungTV:
|
||||
enabled: 0
|
||||
settings:
|
||||
STV_MODEL: STANDARD_13
|
||||
Win:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
Win64:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
WindowsStoreApps:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
DontProcess: False
|
||||
PlaceholderPath: Assets/JsonDotNet/Assemblies/Standalone/Newtonsoft.Json.dll
|
||||
SDK: AnySDK
|
||||
ScriptingBackend: DotNet
|
||||
iOS:
|
||||
enabled: 0
|
||||
settings:
|
||||
CompileFlags:
|
||||
FrameworkDependencies:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -12,6 +12,6 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 20553fac56ec59645857c0732b787431, type: 3}
|
||||
m_Name: OVRBuildConfig
|
||||
m_EditorClassIdentifier:
|
||||
androidSDKPath:
|
||||
androidSDKPath: C:\Users\tvero\AppData\Local\Android\Sdk
|
||||
gradlePath:
|
||||
jdkPath:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace QuestAppLauncher
|
||||
@@ -23,12 +24,12 @@ namespace QuestAppLauncher
|
||||
// File name of app name overrides
|
||||
const string AppNameOverrideFileSearch = "appnames*.txt";
|
||||
|
||||
// File name of excluded package names
|
||||
const string ExcludedPackagesFile = "excludedpackages.txt";
|
||||
|
||||
// Icon pack search string
|
||||
const string IconPackSearch = "iconpack*.zip";
|
||||
|
||||
// File name of excluded package names
|
||||
const string ExcludedPackagesFile = "excludedpackages.txt";
|
||||
|
||||
// Icon pack extraction dir
|
||||
const string IconPackExtractionDir = "cache";
|
||||
|
||||
@@ -117,18 +118,17 @@ namespace QuestAppLauncher
|
||||
Debug.LogFormat("[{0}] package: {1}, name: {2}, auto tab: {3}", i, packageName, appName, tabName);
|
||||
}
|
||||
|
||||
// Process appname*.txt files, sorted by name
|
||||
foreach (var appNameOverrideFilePath in Directory.GetFiles(
|
||||
persistentDataPath, AppNameOverrideFileSearch).OrderBy(f => f))
|
||||
{
|
||||
ProcessAppNameOverrideFile(apps, appNameOverrideFilePath);
|
||||
}
|
||||
// Process app name overrides files (both downloaded & manually created)
|
||||
ProcessAppNameOverrideFiles(apps, AssetsDownloader.GetOrCreateDownloadPath());
|
||||
ProcessAppNameOverrideFiles(apps, persistentDataPath);
|
||||
|
||||
// Extract icon packs
|
||||
ExtractIconPacks(currentActivity);
|
||||
// Extract icon packs (both downloaded & manually created)
|
||||
ExtractIconPacks(currentActivity, AssetsDownloader.GetOrCreateDownloadPath());
|
||||
ExtractIconPacks(currentActivity, persistentDataPath);
|
||||
|
||||
// Process extracted icons
|
||||
ProcessExtractedIcons(apps);
|
||||
// Process extracted icons (both downloaded & manually created)
|
||||
ProcessExtractedIcons(apps, AssetsDownloader.GetOrCreateDownloadPath());
|
||||
ProcessExtractedIcons(apps, persistentDataPath);
|
||||
|
||||
// Process any individual icons
|
||||
var iconOverridePath = persistentDataPath;
|
||||
@@ -141,6 +141,16 @@ namespace QuestAppLauncher
|
||||
return apps;
|
||||
}
|
||||
|
||||
private static void ProcessAppNameOverrideFiles(Dictionary<string, ProcessedApp> apps, string path)
|
||||
{
|
||||
// Process appname*.txt files, sorted by name
|
||||
foreach (var filePath in Directory.GetFiles(
|
||||
path, AppNameOverrideFileSearch).OrderBy(f => f))
|
||||
{
|
||||
ProcessAppNameOverrideFile(apps, filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessAppNameOverrideFile(Dictionary<string, ProcessedApp> apps, string appNameOverrideFilePath)
|
||||
{
|
||||
// Override app names, if any
|
||||
@@ -222,16 +232,15 @@ namespace QuestAppLauncher
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExtractIconPacks(AndroidJavaObject currentActivity)
|
||||
private static void ExtractIconPacks(AndroidJavaObject currentActivity, string iconPacksPath)
|
||||
{
|
||||
var iconPackDestinationFolders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var iconPacksPath = UnityEngine.Application.persistentDataPath;
|
||||
|
||||
if (!Directory.Exists(iconPacksPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var iconPackDestinationFolders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Full path of extraction dir
|
||||
var extractionDirPath = Path.Combine(iconPacksPath, IconPackExtractionDir);
|
||||
Debug.LogFormat("Extraction dir: {0}", extractionDirPath);
|
||||
@@ -278,10 +287,10 @@ namespace QuestAppLauncher
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessExtractedIcons(Dictionary<string, ProcessedApp> apps)
|
||||
private static void ProcessExtractedIcons(Dictionary<string, ProcessedApp> apps, string iconsPath)
|
||||
{
|
||||
// Full path of extraction dir
|
||||
var extractionDirPath = Path.Combine(UnityEngine.Application.persistentDataPath, IconPackExtractionDir);
|
||||
var extractionDirPath = Path.Combine(iconsPath, IconPackExtractionDir);
|
||||
|
||||
if (Directory.Exists(extractionDirPath))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,466 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
using UnityEngine.SceneManagement;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace QuestAppLauncher
|
||||
{
|
||||
/// <summary>
|
||||
/// Downloads assets (app icons packs and names files) from configured repos.
|
||||
/// </summary>
|
||||
public class AssetsDownloader
|
||||
{
|
||||
// Download cache folder that contains downloaded files
|
||||
const string DownloadCacheFolder = "download_cache";
|
||||
|
||||
// Manifest file to track what we've downloaded
|
||||
const string DownloadManifestFile = "download_manifest.json";
|
||||
|
||||
// GitHub API url
|
||||
const string GithubApiUrl = @"http://api.github.com/repos/";
|
||||
|
||||
// Rate limit in minutes
|
||||
const int RateLimitInMins = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Class that tracks each file downloaded.
|
||||
/// Used to determine whether an asset has been updated since last download.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class AssetInfo
|
||||
{
|
||||
// Repo uri
|
||||
public string RepoUri;
|
||||
|
||||
// Asset's url
|
||||
public string Url;
|
||||
|
||||
// Asset's updated at timestamp
|
||||
public string UpdatedAt;
|
||||
|
||||
// Asset's tag name
|
||||
public string TagName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mapping of asset name to asset info above. This is persisted in the download manifest.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class AssetsManifest
|
||||
{
|
||||
// File name -> release metadata
|
||||
public Dictionary<string, AssetInfo> Metadata = new Dictionary<string, AssetInfo>();
|
||||
|
||||
// Last updated timestamp
|
||||
public DateTime LastUpdated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to download assets asynchronously. After completion, scene is automatically reloaded
|
||||
/// if new assets have been downloaded.
|
||||
/// </summary>
|
||||
/// <param name="config">Current config</param>
|
||||
/// <param name="downloadProgress">Download progress interface - used to indicate download progress</param>
|
||||
/// <returns></returns>
|
||||
public static async Task DownloadAssetsAsync(Config config, IDownloadProgress downloadProgress = null, bool forceCheck = false)
|
||||
{
|
||||
// Start background thread
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// Attach / detatch JNI. Required for any calls into JNI from background threads.
|
||||
AndroidJNI.AttachCurrentThread();
|
||||
|
||||
try
|
||||
{
|
||||
// Download assets from repos.
|
||||
AssetsDownloader assetsDownloader = new AssetsDownloader();
|
||||
return await assetsDownloader.DownloadFromReposAsync(config, downloadProgress, forceCheck);
|
||||
}
|
||||
finally
|
||||
{
|
||||
AndroidJNI.DetachCurrentThread();
|
||||
}
|
||||
}).ContinueWith((downloadedAssets) =>
|
||||
{
|
||||
if (downloadedAssets.Result)
|
||||
{
|
||||
// We downloaded new assets, so re-load the scene
|
||||
Debug.Log("Downloaded new assets. Re-populating panel");
|
||||
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously download assets from configured repos.
|
||||
/// </summary>
|
||||
/// <param name="config">Current config</param>
|
||||
/// <param name="downloadProgress">Download progress interface</param>
|
||||
/// <returns></returns>
|
||||
private async Task<bool> DownloadFromReposAsync(Config config, IDownloadProgress downloadProgress = null, bool forceCheck = false)
|
||||
{
|
||||
if (null == config.downloadRepos)
|
||||
{
|
||||
// No repos configured, so return
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load the download manifest. This is used to compare if we're up-to-date or not.
|
||||
AssetsManifest manifest = LoadManifest();
|
||||
if (null == manifest)
|
||||
{
|
||||
manifest = new AssetsManifest();
|
||||
}
|
||||
|
||||
// Rate limit update checks to one per couple of minutes, to avoid GitHub's rate limit
|
||||
if (!forceCheck && DateTime.Now.Subtract(manifest.LastUpdated).TotalMinutes < RateLimitInMins)
|
||||
{
|
||||
Debug.LogFormat("Exceeded rate limit of {0} - last checked for update on {1}", RateLimitInMins, manifest.LastUpdated);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark that we've just checked for updates & update manifest
|
||||
manifest.LastUpdated = DateTime.Now;
|
||||
SaveManifest(manifest);
|
||||
|
||||
if (null != downloadProgress)
|
||||
{
|
||||
downloadProgress.OnCheckingForUpdates();
|
||||
}
|
||||
|
||||
// Download the assets metadata - used to determine whether we are up-to-date or not
|
||||
var assetsInfo = await DownloadAssetsMetadata(config, manifest, downloadProgress, forceCheck);
|
||||
if (assetsInfo.Count == 0)
|
||||
{
|
||||
// No updates have been found, so return
|
||||
Debug.Log("No updates found");
|
||||
if (null != downloadProgress)
|
||||
{
|
||||
downloadProgress.OnNoUpdatesAvailable();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Download the assets
|
||||
var downloadedAssets = await DownloadFromReposInternalAsync(manifest, assetsInfo, downloadProgress);
|
||||
if (null != downloadProgress)
|
||||
{
|
||||
downloadProgress.OnUpdateFinish();
|
||||
}
|
||||
|
||||
return downloadedAssets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads assets metadata. This is used to determine whether our assets are up-to-date.
|
||||
/// </summary>
|
||||
/// <param name="config">Current config</param>
|
||||
/// <param name="downloadProgress">Download progress interface</param>
|
||||
/// <returns></returns>
|
||||
private async Task<Dictionary<string, AssetInfo>> DownloadAssetsMetadata(
|
||||
Config config, AssetsManifest manifest, IDownloadProgress downloadProgress = null, bool forceCheck = false)
|
||||
{
|
||||
// Get asset info from repos
|
||||
var assetsInfo = new Dictionary<string, AssetInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
var reposLoaded = new HashSet<string>();
|
||||
foreach (var item in config.downloadRepos)
|
||||
{
|
||||
if (null == item.type || !string.Equals(item.type, Config.DownloadRepo_Type_GitHub, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// For now, we only support GitHub repos
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get assets from the GitHub repo
|
||||
var repoLoaded = await GetAssetsInfoFromGithubRepoAsync(item.repoUri, assetsInfo, downloadProgress);
|
||||
if (repoLoaded)
|
||||
{
|
||||
reposLoaded.Add(item.repoUri);
|
||||
}
|
||||
}
|
||||
|
||||
Debug.LogFormat("Assets info contains {0} entries", assetsInfo.Count);
|
||||
|
||||
// Enumerate asset metadata in our download manifest
|
||||
var deletedAssets = new HashSet<string>();
|
||||
foreach (var entry in manifest.Metadata)
|
||||
{
|
||||
if (assetsInfo.ContainsKey(entry.Key))
|
||||
{
|
||||
if (string.Equals(entry.Value.UpdatedAt, assetsInfo[entry.Key].UpdatedAt, StringComparison.OrdinalIgnoreCase) &&
|
||||
File.Exists(Path.Combine(GetOrCreateDownloadPath(), entry.Key)))
|
||||
{
|
||||
// Matched on file name and updated-at timestamp, so we're up to date. Skip this one.
|
||||
Debug.LogFormat("Asset {0} is already up to date ({1})", entry.Key, entry.Value.UpdatedAt);
|
||||
assetsInfo.Remove(entry.Key);
|
||||
}
|
||||
}
|
||||
else if (reposLoaded.Contains(entry.Value.RepoUri))
|
||||
{
|
||||
// The repo no longer has the cached file, so nuke it to keep things in sync
|
||||
var filePath = Path.Combine(GetOrCreateDownloadPath(), entry.Key);
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
Debug.LogFormat("Deleting cached file that no longer exists on server: {0} @ {1}", filePath, entry.Value.RepoUri);
|
||||
File.Delete(filePath);
|
||||
}
|
||||
deletedAssets.Add(entry.Key);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove old cached files
|
||||
foreach (var entry in deletedAssets)
|
||||
{
|
||||
manifest.Metadata.Remove(entry);
|
||||
}
|
||||
|
||||
if (deletedAssets.Count > 0)
|
||||
{
|
||||
// Persist manifest
|
||||
SaveManifest(manifest);
|
||||
}
|
||||
|
||||
return assetsInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download assets from repos. The download manifest is also updated.
|
||||
/// </summary>
|
||||
/// <param name="manifest">Download manifest</param>
|
||||
/// <param name="assetsInfo">Assets to download</param>
|
||||
/// <param name="downloadProgress">Download progress interface</param>
|
||||
/// <returns></returns>
|
||||
private async Task<bool> DownloadFromReposInternalAsync(
|
||||
AssetsManifest manifest, Dictionary<string, AssetInfo> assetsInfo, IDownloadProgress downloadProgress = null)
|
||||
{
|
||||
var downloadedAsset = false;
|
||||
|
||||
foreach (var entry in assetsInfo)
|
||||
{
|
||||
// Download asset
|
||||
var success = await DownloadAssetFromGitHubRepoAsync(entry.Key, entry.Value, downloadProgress);
|
||||
if (success)
|
||||
{
|
||||
// Update manifest
|
||||
manifest.Metadata[entry.Key] = entry.Value;
|
||||
downloadedAsset = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadedAsset)
|
||||
{
|
||||
// Persist manifest
|
||||
SaveManifest(manifest);
|
||||
}
|
||||
|
||||
return downloadedAsset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download asset metadata from given uri. The assets info parameter is populate with the metadata.
|
||||
/// </summary>
|
||||
/// <param name="repoUri">The repo uri to download</param>
|
||||
/// <param name="assetsInfo">Assets info mapping that is populate by this function</param>
|
||||
/// <param name="downloadProgress">Download progress interface</param>
|
||||
/// <returns></returns>
|
||||
private async Task<bool> GetAssetsInfoFromGithubRepoAsync(string repoUri,
|
||||
Dictionary<string, AssetInfo> assetsInfo, IDownloadProgress downloadProgress = null)
|
||||
{
|
||||
var requestUrl = GithubApiUrl + repoUri;
|
||||
Debug.LogFormat("Reading assets from {0}", requestUrl);
|
||||
|
||||
try
|
||||
{
|
||||
// Request asset url
|
||||
using (var req = new UnityWebRequest(requestUrl))
|
||||
{
|
||||
req.downloadHandler = new DownloadHandlerBuffer();
|
||||
await req.SendWebRequest();
|
||||
if (req.isNetworkError || req.isHttpError)
|
||||
{
|
||||
// Error reading asset metadata, so return.
|
||||
Debug.LogFormat("Error reading asset info: {0}", req.error);
|
||||
|
||||
var responseHeaders = req.GetResponseHeaders();
|
||||
if (null != responseHeaders && responseHeaders.ContainsKey("X-RateLimit-Remaining") &&
|
||||
responseHeaders["X-RateLimit-Remaining"] == "0")
|
||||
{
|
||||
// Github request limit reached. Display a friendly error message.
|
||||
Debug.LogFormat("Request limit reached");
|
||||
|
||||
if (null != downloadProgress)
|
||||
{
|
||||
downloadProgress.OnError(string.Format("Error updating: Request Limit Reached - try again later. ({1})",
|
||||
req.error, requestUrl));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (null != downloadProgress)
|
||||
{
|
||||
downloadProgress.OnError(string.Format("Error updating: {0} ({1})",
|
||||
req.error, requestUrl));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse the returned asset metadata
|
||||
var jObject = JObject.Parse(req.downloadHandler.text);
|
||||
var tagName = jObject["tag_name"].Value<string>();
|
||||
|
||||
foreach (var property in jObject["assets"])
|
||||
{
|
||||
var updatedAt = property["updated_at"].Value<string>();
|
||||
var url = property["url"].Value<string>();
|
||||
var name = property["name"].Value<string>();
|
||||
|
||||
// For now, simply accept any iconpack*.zip and appnames*.txt.
|
||||
if ((name.StartsWith("iconpack") && name.EndsWith(".zip")) ||
|
||||
(name.StartsWith("appnames") && name.EndsWith(".txt")))
|
||||
{
|
||||
assetsInfo[name] = new AssetInfo { RepoUri = repoUri, Url = url, UpdatedAt = updatedAt, TagName = tagName };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogFormat("Exception reading asset info: {0} ", e.Message);
|
||||
if (null != downloadProgress)
|
||||
{
|
||||
downloadProgress.OnError(string.Format("Error updating: {0} ({1})",
|
||||
e.Message, requestUrl));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download a single asset (file) from repo
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the asset (file) to download</param>
|
||||
/// <param name="assetInfo">Asset info (metadata)</param>
|
||||
/// <param name="downloadProgress">Download progress interface</param>
|
||||
/// <returns></returns>
|
||||
private async Task<bool> DownloadAssetFromGitHubRepoAsync(string name, AssetInfo assetInfo,
|
||||
IDownloadProgress downloadProgress)
|
||||
{
|
||||
var filePath = Path.Combine(GetOrCreateDownloadPath(), name);
|
||||
Debug.LogFormat("Downloading asset {0} from {1}", filePath, assetInfo.Url);
|
||||
|
||||
try
|
||||
{
|
||||
// Request asset url
|
||||
using (var req = new UnityWebRequest(assetInfo.Url))
|
||||
{
|
||||
req.SetRequestHeader("Accept", "application/octet-stream");
|
||||
if (null != downloadProgress)
|
||||
{
|
||||
downloadProgress.OnDownloadStart(name);
|
||||
}
|
||||
var downloadHandler = new DownloadHandlerFileWithProgress(filePath, downloadProgress.OnDownloadProgress);
|
||||
downloadHandler.removeFileOnAbort = true;
|
||||
req.downloadHandler = downloadHandler;
|
||||
await req.SendWebRequest();
|
||||
|
||||
if (req.isNetworkError || req.isHttpError)
|
||||
{
|
||||
// Error reading asset metadata, so return.
|
||||
Debug.LogFormat("Error downloading asset: {0}", req.error);
|
||||
if (null != downloadProgress)
|
||||
{
|
||||
downloadProgress.OnError(string.Format("Error updating: {0} ({1})",
|
||||
req.error, assetInfo.Url));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null != downloadProgress)
|
||||
{
|
||||
downloadProgress.OnDownloadFinish();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogFormat("Exception downloading asset: {0} ", e.Message);
|
||||
if (null != downloadProgress)
|
||||
{
|
||||
downloadProgress.OnError(string.Format("Error updating: {0} ({1})",
|
||||
e.Message, assetInfo.Url));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load download manifest
|
||||
/// </summary>
|
||||
/// <returns>Download manifest</returns>
|
||||
static private AssetsManifest LoadManifest()
|
||||
{
|
||||
var manifestPath = Path.Combine(GetOrCreateDownloadPath(), DownloadManifestFile);
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Debug.Log("Found manifest: " + manifestPath);
|
||||
var jsonManifest = File.ReadAllText(manifestPath);
|
||||
|
||||
try
|
||||
{
|
||||
AssetsManifest manifest = new AssetsManifest();
|
||||
return JsonConvert.DeserializeObject<AssetsManifest>(jsonManifest);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.Log(string.Format("Failed to read manifest: {0}", e.Message));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persist download manifest
|
||||
/// </summary>
|
||||
/// <param name="manifest">Download manifest to persist</param>
|
||||
static private void SaveManifest(AssetsManifest manifest)
|
||||
{
|
||||
var manifestPath = Path.Combine(GetOrCreateDownloadPath(), DownloadManifestFile);
|
||||
Debug.Log("Saving manifest: " + manifestPath);
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(manifestPath, JsonConvert.SerializeObject(manifest, Formatting.Indented));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.Log(string.Format("Failed to write manifest: {0}", e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
static public string GetOrCreateDownloadPath()
|
||||
{
|
||||
string path = Path.Combine(UnityEngine.Application.persistentDataPath, DownloadCacheFolder);
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6d9bad1aeba3c1448c9d931adb9ee97
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -3,6 +3,8 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace QuestAppLauncher
|
||||
{
|
||||
@@ -18,6 +20,10 @@ namespace QuestAppLauncher
|
||||
public const string Category_Left = "left";
|
||||
public const string Category_Right = "right";
|
||||
|
||||
// Download repos
|
||||
public const string DownloadRepo_Type_GitHub = "github";
|
||||
public const string DownloadRepo_Default = @"tverona1/QuestAppLauncher_Assets/releases/latest";
|
||||
|
||||
/// <summary>
|
||||
/// Grid size
|
||||
/// </summary>
|
||||
@@ -28,6 +34,16 @@ namespace QuestAppLauncher
|
||||
public int cols = 3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download repo type
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class DownloadRepo
|
||||
{
|
||||
public string repoUri;
|
||||
public string type;
|
||||
}
|
||||
|
||||
// Grid size, specified as cols x rows
|
||||
public GridSize gridSize = new GridSize();
|
||||
|
||||
@@ -39,6 +55,15 @@ namespace QuestAppLauncher
|
||||
|
||||
// Custom Category: Apps are categorized according to appnames.txt file
|
||||
public string customCategory = Category_Right;
|
||||
|
||||
// Whether to auto-download updates
|
||||
public bool autoUpdate = false;
|
||||
|
||||
// Github download repos
|
||||
public List<DownloadRepo> downloadRepos = new List<DownloadRepo>()
|
||||
{
|
||||
new DownloadRepo { repoUri = DownloadRepo_Default, type = DownloadRepo_Type_GitHub }
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -52,8 +77,9 @@ namespace QuestAppLauncher
|
||||
/// <summary>
|
||||
/// Load config from file
|
||||
/// </summary>
|
||||
/// <param name="config">Config object that will be overwritten</param>
|
||||
static public void LoadConfig(Config config)
|
||||
/// <param name="config"></param>
|
||||
/// <returns>Config object</returns>
|
||||
static public Config LoadConfig()
|
||||
{
|
||||
var configFilePath = Path.Combine(UnityEngine.Application.persistentDataPath, ConfigFileName);
|
||||
if (File.Exists(configFilePath))
|
||||
@@ -63,7 +89,7 @@ namespace QuestAppLauncher
|
||||
|
||||
try
|
||||
{
|
||||
JsonUtility.FromJsonOverwrite(jsonConfig, config);
|
||||
return JsonConvert.DeserializeObject<Config>(jsonConfig);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -74,6 +100,9 @@ namespace QuestAppLauncher
|
||||
{
|
||||
Debug.Log("Did not find config file: " + configFilePath);
|
||||
}
|
||||
|
||||
// Return default config
|
||||
return new Config();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -87,11 +116,11 @@ namespace QuestAppLauncher
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(configFilePath, JsonUtility.ToJson(config, true));
|
||||
File.WriteAllText(configFilePath, JsonConvert.SerializeObject(config, Formatting.Indented));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.Log(string.Format("Failed to read config: {0}", e.Message));
|
||||
Debug.Log(string.Format("Failed to write config: {0}", e.Message));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace QuestAppLauncher
|
||||
{
|
||||
/// <summary>
|
||||
/// File download handler class with callback for progress.
|
||||
/// This is a download hanlder that can be assocaited with a UnityWebRequest.
|
||||
/// Based on: DownloadHandlerFile.cs by Luke Holland
|
||||
/// (https://gist.github.com/luke161/a251b01c00f58d65a252812be8dce670)
|
||||
/// </summary>
|
||||
public class DownloadHandlerFileWithProgress : DownloadHandlerScript
|
||||
{
|
||||
// Whether to remove file on error
|
||||
public bool removeFileOnAbort = false;
|
||||
|
||||
// Total content length in bytes
|
||||
private int contentLength;
|
||||
|
||||
// Bytes recieved so far
|
||||
private int received;
|
||||
|
||||
// File stream to write to
|
||||
private FileStream stream;
|
||||
|
||||
// Callback to incidate progress: Percentage, total content length and current received
|
||||
private Action<float, int, int> downloadProgress;
|
||||
|
||||
// Whether we have successfully received all the data
|
||||
private bool receivedAllData = false;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for the file download handler. Instantiates base class in pre-allocated mode.
|
||||
/// </summary>
|
||||
/// <param name="localFilePath">Local path to save file to</param>
|
||||
/// <param name="downloadProgress">Download progress indicator</param>
|
||||
/// <param name="bufferSize">Buffer size</param>
|
||||
/// <param name="fileShare">File sharing mode</param>
|
||||
public DownloadHandlerFileWithProgress(string localFilePath, Action<float, int, int> downloadProgress = null,
|
||||
int bufferSize = 32768, FileShare fileShare = FileShare.ReadWrite) : base(new byte[bufferSize])
|
||||
{
|
||||
string directory = Path.GetDirectoryName(localFilePath);
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
this.downloadProgress = downloadProgress;
|
||||
this.contentLength = -1;
|
||||
this.received = 0;
|
||||
this.stream = new FileStream(localFilePath, FileMode.OpenOrCreate, FileAccess.Write, fileShare, bufferSize);
|
||||
}
|
||||
|
||||
protected override float GetProgress()
|
||||
{
|
||||
return contentLength <= 0 ? 0 : Mathf.Clamp01((float)this.received / (float)contentLength);
|
||||
}
|
||||
|
||||
protected override void ReceiveContentLength(int contentLength)
|
||||
{
|
||||
this.contentLength = contentLength;
|
||||
}
|
||||
|
||||
protected override bool ReceiveData(byte[] data, int dataLength)
|
||||
{
|
||||
if (data == null || data.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this.received += dataLength;
|
||||
this.stream.Write(data, 0, dataLength);
|
||||
|
||||
if (null != this.downloadProgress)
|
||||
{
|
||||
this.downloadProgress(GetProgress(), this.contentLength, this.received);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void CompleteContent()
|
||||
{
|
||||
this.receivedAllData = true;
|
||||
CloseStream();
|
||||
}
|
||||
|
||||
public new void Dispose()
|
||||
{
|
||||
CloseStream();
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
private void CloseStream()
|
||||
{
|
||||
string filePath = this.stream.Name;
|
||||
if (this.stream != null)
|
||||
{
|
||||
this.stream.Dispose();
|
||||
this.stream = null;
|
||||
}
|
||||
|
||||
if (this.removeFileOnAbort && !this.receivedAllData && File.Exists(filePath))
|
||||
{
|
||||
// We have not successfully received all the content, so delete the file
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12b34f1926e99e742a01d3c725d778df
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,182 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
namespace QuestAppLauncher
|
||||
{
|
||||
/// <summary>
|
||||
/// Script to display download status
|
||||
/// </summary>
|
||||
public class DownloadStatusIndicator : MonoBehaviour, IDownloadProgress
|
||||
{
|
||||
/// <summary>
|
||||
/// State of the download
|
||||
/// </summary>
|
||||
enum State
|
||||
{
|
||||
None,
|
||||
DownloadStart,
|
||||
DownloadProgress,
|
||||
DownloadProgressUpdated,
|
||||
DownloadFinish,
|
||||
Error,
|
||||
Checking,
|
||||
NoUpdates,
|
||||
UpdateFinished
|
||||
};
|
||||
|
||||
// Text gui
|
||||
private TextMeshProUGUI textGui;
|
||||
|
||||
// Current & previous state. The callback may be invoked from non-main thread, so we use a simple
|
||||
// state machine to track the state.
|
||||
private State prevState = State.None;
|
||||
private State state = State.None;
|
||||
private readonly object stateLock = new object();
|
||||
|
||||
// State information
|
||||
private string name;
|
||||
private float progress;
|
||||
private string errorMessage;
|
||||
|
||||
// Start is called before the first frame update
|
||||
void Start()
|
||||
{
|
||||
this.textGui = GetComponent<TextMeshProUGUI>();
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
async void Update()
|
||||
{
|
||||
bool clearText = false;
|
||||
|
||||
if (this.state == this.prevState)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (this.stateLock)
|
||||
{
|
||||
// Update UI based on state
|
||||
switch (this.state)
|
||||
{
|
||||
case State.DownloadStart:
|
||||
this.textGui.text = string.Format("Downloading {0}", this.name);
|
||||
break;
|
||||
case State.DownloadFinish:
|
||||
this.textGui.text = string.Format("Downloading {0} [100%]", this.name);
|
||||
break;
|
||||
case State.Checking:
|
||||
this.textGui.text = string.Format("Checking for updates");
|
||||
break;
|
||||
case State.UpdateFinished:
|
||||
// End state - display last error if available & clear text after a delay
|
||||
if (null == this.errorMessage)
|
||||
{
|
||||
this.textGui.text = string.Format("Update complete");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.textGui.text = this.errorMessage;
|
||||
}
|
||||
clearText = true;
|
||||
break;
|
||||
case State.NoUpdates:
|
||||
// End state - display last error if available & clear text after a delay
|
||||
if (null == this.errorMessage)
|
||||
{
|
||||
this.textGui.text = string.Format("No updates available");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.textGui.text = this.errorMessage;
|
||||
}
|
||||
clearText = true;
|
||||
break;
|
||||
case State.Error:
|
||||
this.textGui.text = this.errorMessage;
|
||||
break;
|
||||
case State.DownloadProgress:
|
||||
this.textGui.text = string.Format("Downloading {0} [{1:0}%]", this.name, this.progress * 100);
|
||||
this.state = State.DownloadProgressUpdated;
|
||||
break;
|
||||
}
|
||||
|
||||
this.prevState = this.state;
|
||||
}
|
||||
|
||||
if (clearText)
|
||||
{
|
||||
await ClearText();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDownloadStart(string name)
|
||||
{
|
||||
lock (this.stateLock)
|
||||
{
|
||||
this.name = name;
|
||||
this.state = State.DownloadStart;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDownloadProgress(float progressPercentage, int totalContent, int receivedContent)
|
||||
{
|
||||
lock (this.stateLock)
|
||||
{
|
||||
this.state = State.DownloadProgress;
|
||||
this.progress = progressPercentage;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDownloadFinish()
|
||||
{
|
||||
lock (this.stateLock)
|
||||
{
|
||||
this.state = State.DownloadFinish;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnError(string message)
|
||||
{
|
||||
lock (this.stateLock)
|
||||
{
|
||||
this.state = State.Error;
|
||||
this.errorMessage = message;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnNoUpdatesAvailable()
|
||||
{
|
||||
lock (this.stateLock)
|
||||
{
|
||||
this.state = State.NoUpdates;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnCheckingForUpdates()
|
||||
{
|
||||
lock (this.stateLock)
|
||||
{
|
||||
this.state = State.Checking;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnUpdateFinish()
|
||||
{
|
||||
lock (this.stateLock)
|
||||
{
|
||||
this.state = State.UpdateFinished;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ClearText()
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
this.textGui.text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91f098ea5c7fe084c8f890ad453a103c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,35 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
|
||||
namespace QuestAppLauncher
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple class that holds state that survives scene reloads.
|
||||
/// Follows Unity's DontDestroyOnLoad singleton pattern.
|
||||
/// </summary>
|
||||
public class GlobalState : MonoBehaviour
|
||||
{
|
||||
public static GlobalState Instance;
|
||||
|
||||
// State to indicate whether we already checked for update
|
||||
public bool CheckedForUpdate = false;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
this.InstantiateController();
|
||||
}
|
||||
|
||||
private void InstantiateController()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(this);
|
||||
}
|
||||
else if (this != Instance)
|
||||
{
|
||||
Destroy(this.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f8767936bbc48140a66cc60c561da2f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -36,6 +36,9 @@ namespace QuestAppLauncher
|
||||
// Tracking space
|
||||
public GameObject trackingSpace;
|
||||
|
||||
// Download status indicator
|
||||
public DownloadStatusIndicator downloadStatusIndicator;
|
||||
|
||||
// App info prefab (a cell in the grid content)
|
||||
public GameObject prefabCell;
|
||||
|
||||
@@ -73,8 +76,7 @@ namespace QuestAppLauncher
|
||||
private async Task PopulateAsync()
|
||||
{
|
||||
// Load configuration
|
||||
Config config = new Config();
|
||||
ConfigPersistence.LoadConfig(config);
|
||||
var config = ConfigPersistence.LoadConfig();
|
||||
|
||||
// Process apps in background
|
||||
var apps = await Task.Run(() =>
|
||||
@@ -91,6 +93,13 @@ namespace QuestAppLauncher
|
||||
}
|
||||
});
|
||||
|
||||
// Download updates in the background
|
||||
if (config.autoUpdate && !GlobalState.Instance.CheckedForUpdate)
|
||||
{
|
||||
GlobalState.Instance.CheckedForUpdate = true;
|
||||
AssetsDownloader.DownloadAssetsAsync(config, this.downloadStatusIndicator);
|
||||
}
|
||||
|
||||
// Populate the panel content
|
||||
await PopulatePanelContentAsync(config, apps);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace QuestAppLauncher
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface to determine download progress
|
||||
/// </summary>
|
||||
public interface IDownloadProgress
|
||||
{
|
||||
void OnCheckingForUpdates();
|
||||
void OnNoUpdatesAvailable();
|
||||
void OnUpdateFinish();
|
||||
|
||||
void OnDownloadStart(string name);
|
||||
void OnDownloadProgress(float progressPercentage, int totalContent, int receivedContent);
|
||||
void OnDownloadFinish();
|
||||
|
||||
void OnError(string message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2df4b12ce2208543a80ccef9c388114
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -20,6 +20,8 @@ namespace QuestAppLauncher
|
||||
public GameObject gridRowsText;
|
||||
public GameObject gridPopulation;
|
||||
public GameObject show2DToggle;
|
||||
public GameObject autoUpdateToggle;
|
||||
public DownloadStatusIndicator downloadStatusIndicator;
|
||||
|
||||
public Toggle tabsAutoOff;
|
||||
public Toggle tabsAutoTop;
|
||||
@@ -33,7 +35,7 @@ namespace QuestAppLauncher
|
||||
|
||||
private bool deletedHiddenAppsFile = false;
|
||||
|
||||
private Config config = new Config();
|
||||
private Config config = null;
|
||||
|
||||
public void OpenSettings()
|
||||
{
|
||||
@@ -44,7 +46,7 @@ namespace QuestAppLauncher
|
||||
this.settingsContainer.SetActive(true);
|
||||
|
||||
// Load config
|
||||
ConfigPersistence.LoadConfig(this.config);
|
||||
this.config = ConfigPersistence.LoadConfig();
|
||||
|
||||
// Set current cols & rows
|
||||
var colsSlider = this.gridCols.GetComponent<Slider>();
|
||||
@@ -61,6 +63,9 @@ namespace QuestAppLauncher
|
||||
// Set 2D toggle
|
||||
this.show2DToggle.GetComponent<Toggle>().SetIsOnWithoutNotify(this.config.show2D);
|
||||
|
||||
// Set auto-update toggle
|
||||
this.autoUpdateToggle.GetComponent<Toggle>().SetIsOnWithoutNotify(this.config.autoUpdate);
|
||||
|
||||
// Set auto tab mode
|
||||
if (this.config.autoCategory.Equals(Config.Category_Top, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -110,6 +115,11 @@ namespace QuestAppLauncher
|
||||
this.settingsContainer.SetActive(false);
|
||||
}
|
||||
|
||||
public void UpdateAssetsNow()
|
||||
{
|
||||
AssetsDownloader.DownloadAssetsAsync(config, this.downloadStatusIndicator, true);
|
||||
}
|
||||
|
||||
public void DeleteExcludedApksFile()
|
||||
{
|
||||
Debug.Log("Delete Excluded App List");
|
||||
@@ -158,6 +168,14 @@ namespace QuestAppLauncher
|
||||
saveConfig = true;
|
||||
}
|
||||
|
||||
// Update auto-update toggle
|
||||
var autoUpdate = this.autoUpdateToggle.GetComponent<Toggle>().isOn;
|
||||
if (autoUpdate != this.config.autoUpdate)
|
||||
{
|
||||
this.config.autoUpdate = autoUpdate;
|
||||
saveConfig = true;
|
||||
}
|
||||
|
||||
// Update auto tab mode
|
||||
string tabAutoMode;
|
||||
if (this.tabsAutoTop.isOn)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace QuestAppLauncher
|
||||
{
|
||||
/// <summary>
|
||||
/// Awaiter implementation for UnityWebRequest - allows us to use async / await pattern on UnityWebRequest.
|
||||
/// </summary>
|
||||
public class UnityWebRequestAwaiter : INotifyCompletion
|
||||
{
|
||||
private UnityWebRequestAsyncOperation asyncOp;
|
||||
private Action continuation;
|
||||
|
||||
public UnityWebRequestAwaiter(UnityWebRequestAsyncOperation asyncOp)
|
||||
{
|
||||
this.asyncOp = asyncOp;
|
||||
asyncOp.completed += OnRequestCompleted;
|
||||
}
|
||||
|
||||
public bool IsCompleted { get { return asyncOp.isDone; } }
|
||||
|
||||
public void GetResult() { }
|
||||
|
||||
public void OnCompleted(Action continuation)
|
||||
{
|
||||
this.continuation = continuation;
|
||||
}
|
||||
|
||||
private void OnRequestCompleted(AsyncOperation obj)
|
||||
{
|
||||
continuation();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ExtensionMethods
|
||||
{
|
||||
public static UnityWebRequestAwaiter GetAwaiter(this UnityWebRequestAsyncOperation asyncOp)
|
||||
{
|
||||
return new UnityWebRequestAwaiter(asyncOp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab2a840feb98f634b9fdb7222ae7707c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
@@ -0,0 +1,91 @@
|
||||
fileFormatVersion: 2
|
||||
guid: efc72c013d5d2734ea900bf079c4a1d6
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 10
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: -1
|
||||
aniso: -1
|
||||
mipBias: -100
|
||||
wrapU: -1
|
||||
wrapV: -1
|
||||
wrapW: -1
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spritePackingTag:
|
||||
pSDRemoveMatte: 0
|
||||
pSDShowRemoveMatteOption: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,115 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 01674f533e98ba544a7bb592e2f52669
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 10
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 2
|
||||
aniso: 1
|
||||
mipBias: -100
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: -1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 2
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 2
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Android
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 2
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spritePackingTag:
|
||||
pSDRemoveMatte: 0
|
||||
pSDShowRemoveMatteOption: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -279,7 +279,45 @@ PlayerSettings:
|
||||
m_BuildTargetPlatformIcons:
|
||||
- m_BuildTarget: Android
|
||||
m_Icons:
|
||||
- m_Textures: []
|
||||
- m_Textures:
|
||||
- {fileID: 2800000, guid: efc72c013d5d2734ea900bf079c4a1d6, type: 3}
|
||||
m_Width: 192
|
||||
m_Height: 192
|
||||
m_Kind: 0
|
||||
m_SubKind:
|
||||
- m_Textures:
|
||||
- {fileID: 0}
|
||||
m_Width: 144
|
||||
m_Height: 144
|
||||
m_Kind: 0
|
||||
m_SubKind:
|
||||
- m_Textures:
|
||||
- {fileID: 0}
|
||||
m_Width: 96
|
||||
m_Height: 96
|
||||
m_Kind: 0
|
||||
m_SubKind:
|
||||
- m_Textures:
|
||||
- {fileID: 0}
|
||||
m_Width: 72
|
||||
m_Height: 72
|
||||
m_Kind: 0
|
||||
m_SubKind:
|
||||
- m_Textures:
|
||||
- {fileID: 0}
|
||||
m_Width: 48
|
||||
m_Height: 48
|
||||
m_Kind: 0
|
||||
m_SubKind:
|
||||
- m_Textures:
|
||||
- {fileID: 0}
|
||||
m_Width: 36
|
||||
m_Height: 36
|
||||
m_Kind: 0
|
||||
m_SubKind:
|
||||
- m_Textures:
|
||||
- {fileID: 0}
|
||||
- {fileID: 2800000, guid: efc72c013d5d2734ea900bf079c4a1d6, type: 3}
|
||||
m_Width: 432
|
||||
m_Height: 432
|
||||
m_Kind: 2
|
||||
@@ -309,62 +347,38 @@ PlayerSettings:
|
||||
m_Height: 81
|
||||
m_Kind: 2
|
||||
m_SubKind:
|
||||
- m_Textures: []
|
||||
m_Width: 192
|
||||
m_Height: 192
|
||||
m_Kind: 0
|
||||
m_SubKind:
|
||||
- m_Textures: []
|
||||
m_Width: 144
|
||||
m_Height: 144
|
||||
m_Kind: 0
|
||||
m_SubKind:
|
||||
- m_Textures: []
|
||||
m_Width: 96
|
||||
m_Height: 96
|
||||
m_Kind: 0
|
||||
m_SubKind:
|
||||
- m_Textures: []
|
||||
m_Width: 72
|
||||
m_Height: 72
|
||||
m_Kind: 0
|
||||
m_SubKind:
|
||||
- m_Textures: []
|
||||
m_Width: 48
|
||||
m_Height: 48
|
||||
m_Kind: 0
|
||||
m_SubKind:
|
||||
- m_Textures: []
|
||||
m_Width: 36
|
||||
m_Height: 36
|
||||
m_Kind: 0
|
||||
m_SubKind:
|
||||
- m_Textures: []
|
||||
- m_Textures:
|
||||
- {fileID: 2800000, guid: efc72c013d5d2734ea900bf079c4a1d6, type: 3}
|
||||
m_Width: 192
|
||||
m_Height: 192
|
||||
m_Kind: 1
|
||||
m_SubKind:
|
||||
- m_Textures: []
|
||||
- m_Textures:
|
||||
- {fileID: 0}
|
||||
m_Width: 144
|
||||
m_Height: 144
|
||||
m_Kind: 1
|
||||
m_SubKind:
|
||||
- m_Textures: []
|
||||
- m_Textures:
|
||||
- {fileID: 0}
|
||||
m_Width: 96
|
||||
m_Height: 96
|
||||
m_Kind: 1
|
||||
m_SubKind:
|
||||
- m_Textures: []
|
||||
- m_Textures:
|
||||
- {fileID: 0}
|
||||
m_Width: 72
|
||||
m_Height: 72
|
||||
m_Kind: 1
|
||||
m_SubKind:
|
||||
- m_Textures: []
|
||||
- m_Textures:
|
||||
- {fileID: 0}
|
||||
m_Width: 48
|
||||
m_Height: 48
|
||||
m_Kind: 1
|
||||
m_SubKind:
|
||||
- m_Textures: []
|
||||
- m_Textures:
|
||||
- {fileID: 0}
|
||||
m_Width: 36
|
||||
m_Height: 36
|
||||
m_Kind: 1
|
||||
|
||||
Reference in New Issue
Block a user