Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 0 additions & 131 deletions src/Platforms/SecureFolderFS.UI/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,136 +57,5 @@ public static class FileData
FolderType = Generic
""";
}

public static class Introduction
{
public const string BACKGROUND_WEBVIEW = """
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Fragment Shader</title>
<style>
body, html, canvas {
background: transparent !important;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<canvas id="gl"></canvas>
<script>
const vs = `#version 300 es
in vec2 aPos;
out vec2 vUv;
void main() {
vUv = (aPos + 1.0) * 0.5;
gl_Position = vec4(aPos, 0.0, 1.0);
}
`;

const fs = `#version 300 es
precision highp float;
in vec2 vUv;
out vec4 fragColor;
uniform vec2 iResolution;
uniform float iTime;

float sstep(float e0, float e1, float x) {
return smoothstep(e0, e1, x);
}

void main() {
vec2 uv = vUv;
uv.y = (uv.y - 0.5) * 2.0;

float t = iTime;

// Color Palette (replaced dynamically)
vec3 bgColor = c_bg;
vec3 waveColor = c_wave;
vec3 c = bgColor;

for (int i = 1; i < 8; i++) {
float fi = float(i);
float x = uv.x * 1.6 + t * sin(fi * 3.3) * 0.4 + fi * 4.3;
float f = 0.3 * cos(fi) * sin(x);
float b = sin(uv.x * sin(fi + t * 0.2) * 3.4 + cos(fi + 7.1)) * 0.1 + 0.896;
float y = smoothstep(b, 1.0, 1.0 - abs(uv.y - f)) * (b - 0.8) * 2.5;

float strength = 0.35 + fi * 0.06;
c = mix(c, waveColor, y * strength);
}

fragColor = vec4(c, 1.0);
}
`;

const canvas = document.getElementById('gl');
const gl = canvas.getContext('webgl2', {antialias: true});
if (!gl) {
document.body.innerHTML = "WebGL2 not supported";
} else {
// resize
function resize() {
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.floor(innerWidth * dpr);
canvas.height = Math.floor(innerHeight * dpr);
gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();

function compile(type, src) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(s));
}
return s;
}

const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, vs));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fs));
gl.bindAttribLocation(prog, 0, 'aPos');
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(prog));
}
gl.useProgram(prog);

// fullscreen triangle
const quad = new Float32Array([-1,-1, 3,-1, -1,3]);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);

const iResolution = gl.getUniformLocation(prog, 'iResolution');
const iTime = gl.getUniformLocation(prog, 'iTime');

let start = performance.now();
function frame() {
resize(); // accomodate rotation/size change
gl.uniform2f(iResolution, canvas.width, canvas.height);
gl.uniform1f(iTime, (performance.now() - start) * 0.001);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
</script>
</body>
</html>
""";
}
}
}
1 change: 0 additions & 1 deletion src/Platforms/SecureFolderFS.UI/SecureFolderFS.UI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
<None Remove="Assets\AppAssets\Devices\gpixel_light.png" />
<None Remove="Assets\AppAssets\Devices\iphone_dark.png" />
<None Remove="Assets\AppAssets\Devices\iphone_light.png" />
<None Remove="Assets\AppAssets\Introduction\intro_light_dashboard.png" />
<None Remove="Assets\AppAssets\Shields\error_shield.svg" />
<None Remove="Assets\AppAssets\Shields\error_shield.png" />
<None Remove="Assets\AppAssets\Shields\success_shield.png" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@
using SecureFolderFS.Sdk.Enums;
using SecureFolderFS.Sdk.Models;
using SecureFolderFS.Sdk.Services;
using SecureFolderFS.Sdk.ViewModels.Controls.Components;
using SecureFolderFS.Sdk.ViewModels.Views.Wizard;
using SecureFolderFS.Sdk.ViewModels.Views.Wizard.DataSources;
using SecureFolderFS.Shared;
using SecureFolderFS.Storage.VirtualFileSystem;
using SecureFolderFS.UI.ServiceImplementation;
using static SecureFolderFS.Sdk.Constants.DataSources;

#if !__UNO_SKIA_MACOS__
using System;
using SecureFolderFS.Uno.Platforms.Desktop.ViewModels;
#endif

namespace SecureFolderFS.Uno.Platforms.Desktop.ServiceImplementation
{
/// <inheritdoc cref="IVaultFileSystemService"/>
Expand All @@ -24,8 +30,27 @@ public override async IAsyncEnumerable<IFileSystemInfo> GetFileSystemsAsync([Enu
yield return new SkiaWebDavFileSystem();

#if !__UNO_SKIA_MACOS__
yield return new FuseFileSystem();
// Inside a Flatpak sandbox the FUSE userspace may appear available, but mounts
// are confined to the sandbox's mount namespace and invisible to the host
if (!FuseInstallationViewModel.IsSandboxed)
yield return new FuseFileSystem();
#endif
}

/// <inheritdoc/>
public override async IAsyncEnumerable<ItemInstallationViewModel> GetFileSystemInstallationsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
#if !__UNO_SKIA_MACOS__
// Host packages cannot be installed from inside an application sandbox (e.g., Flatpak)
if (OperatingSystem.IsLinux() && !FuseInstallationViewModel.IsSandboxed)
{
var fuse = new FuseInstallationViewModel();
await fuse.InitAsync(cancellationToken);
yield return fuse;
}
#endif
await Task.CompletedTask;
yield break;
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SecureFolderFS.Sdk.Services;
using SecureFolderFS.Sdk.ViewModels.Controls.Components;
using SecureFolderFS.Shared;
using SecureFolderFS.Shared.Models;
using Tmds.Fuse;

namespace SecureFolderFS.Uno.Platforms.Desktop.ViewModels
{
/// <summary>
/// Installs the FUSE userspace components (libfuse3 and fusermount3) through the
/// distribution's native package manager, elevated with a PolicyKit (pkexec) prompt.
/// The kernel half of FUSE ships with every mainstream distribution, so the 'fuse3'
/// package is all that is needed to make <see cref="Core.FUSE.FuseFileSystem"/> available.
/// </summary>
public sealed partial class FuseInstallationViewModel() : ItemInstallationViewModel(Core.FUSE.Constants.FileSystem.FS_ID, Core.FUSE.Constants.FileSystem.FS_NAME)
{
private const int PKEXEC_DIALOG_DISMISSED = 126; // The user closed the authentication dialog
private const int PKEXEC_NOT_AUTHORIZED = 127;

// Package manager binaries and the arguments that install libfuse3 + fusermount3
private static readonly (string FileName, string Arguments)[] KnownPackageManagers =
[
("apt-get", "install -y fuse3"),
("dnf", "install -y fuse3"),
("yum", "install -y fuse3"),
("pacman", "-S --needed --noconfirm fuse3"),
("zypper", "--non-interactive install fuse3")
];

/// <summary>
/// Gets whether the app runs inside a Flatpak sandbox, where host packages cannot
/// be installed and sandbox-local FUSE mounts would not be visible to the host.
/// </summary>
public static bool IsSandboxed { get; } = File.Exists("/.flatpak-info");

/// <inheritdoc/>
public override async Task InitAsync(CancellationToken cancellationToken = default)
{
var mediaService = DI.Service<IMediaService>();
Icon = await mediaService.GetImageFromResourceAsync(Id, cancellationToken);
}

/// <inheritdoc/>
protected override async Task InstallAsync(CancellationToken cancellationToken)
{
try
{
IsProgressing = true;
IsIndeterminate = true; // Package managers don't expose machine-readable progress

if (FindProgram("pkexec") is null)
throw new InvalidOperationException("PolicyKit (pkexec) was not found. Install the 'fuse3' package manually with your package manager.");

var (packageManager, arguments) = FindInstallCommand()
?? throw new InvalidOperationException("No supported package manager was found. Install the 'fuse3' package manually.");

var exitCode = await RunElevatedAsync(packageManager, arguments, cancellationToken);
switch (exitCode)
{
case 0:
break;

case PKEXEC_DIALOG_DISMISSED:
return; // Dismissing the authentication dialog is deliberate - nothing to report

case PKEXEC_NOT_AUTHORIZED:
throw new UnauthorizedAccessException("Authorization to install the 'fuse3' package was not granted.");

default:
throw new InvalidOperationException($"The 'fuse3' package installation exited with code {exitCode}.");
}

// Confirm that libfuse3 and fusermount3 are actually usable now
if (!Fuse.CheckDependencies())
throw new InvalidOperationException("FUSE is still unavailable after the installation.");

IsInstalled = true;
}
catch (OperationCanceledException)
{
// Cancellation, nothing to report
}
catch (Exception ex)
{
Report(Result.Failure(ex));
}
finally
{
IsProgressing = false;
IsIndeterminate = false;
}
}

private static (string FileName, string Arguments)? FindInstallCommand()
{
foreach (var (fileName, arguments) in KnownPackageManagers)
{
// pkexec resolves programs with its own restricted PATH, so pass the absolute path
if (FindProgram(fileName) is { } fullPath)
return (fullPath, arguments);
}

return null;
}

private static string? FindProgram(string fileName)
{
var pathDirectories = (Environment.GetEnvironmentVariable("PATH") ?? string.Empty)
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);

// Also probe the standard locations in case the app was launched with a minimal PATH
string[] fallbackDirectories = ["/usr/bin", "/usr/sbin", "/bin", "/sbin"];

foreach (var directory in (string[])[.. pathDirectories, .. fallbackDirectories])
{
var candidate = Path.Combine(directory, fileName);
if (File.Exists(candidate))
return candidate;
}

return null;
}

private static async Task<int> RunElevatedAsync(string fileName, string arguments, CancellationToken cancellationToken)
{
var processInfo = new ProcessStartInfo
{
FileName = "pkexec",
Arguments = $"{fileName} {arguments}",
UseShellExecute = false,
CreateNoWindow = true
};

using var process = Process.Start(processInfo)
?? throw new InvalidOperationException("Failed to start the elevated installation process.");

await process.WaitForExitAsync(cancellationToken);
return process.ExitCode;
}
}
}
Loading
Loading