Skip to content
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# 🌊 Teebot Flow

[![teebot-flow](https://snapcraft.io/teebot-flow/badge.svg)](https://snapcraft.io/teebot-flow)

🌐 **Web Version**: [flow.iteebot.com](https://flow.iteebot.com)

**Teebot Flow** is a high-performance, professional Desktop ERP solution designed for modern business operations. Built with a focus on speed, reliability, and ease of use, it leverages the power of **Tauri** and **Rust** to provide a secure, offline-first desktop experience.

Teebot Flow is now open for public collaboration. The project is released under **AGPL-3.0**, and contributions, issue reports, and pull requests are welcome.
Expand Down
102 changes: 73 additions & 29 deletions src/lib/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,46 +45,42 @@ const ASSET_NAMES = {
linux: {
appImage: (version: string) => `Teebot Flow_${version}_amd64.AppImage`,
deb: (version: string) => `Teebot Flow_${version}_amd64.deb`,
rpm: (version: string) => `teebot-flow-${version}-1.x86_64.rpm`,
},
macOS: {
aarch64: (version: string) => `Teebot.Flow_${version}_aarch64.dmg`,
x64: (version: string) => `Teebot.Flow_${version}_x64.dmg`,
},
};

// Cache the computed Mac architecture for the duration of the browser session
let cachedMacArch: "aarch64" | "x64" | null | undefined = undefined;

const getMacArch = (): "aarch64" | "x64" | null => {
if (typeof window === "undefined") return null;
if (cachedMacArch !== undefined) {
return cachedMacArch;
}

if (typeof window === "undefined") {
cachedMacArch = null;
return null;
}

const userAgent = window.navigator.userAgent.toLowerCase();


// Check for explicit, trustworthy architecture signals in the User Agent.
// We do NOT check for generic/spoofed/reduced "intel" or "mac os x" UA strings.
if (userAgent.includes("arm64") || userAgent.includes("aarch64")) {
cachedMacArch = "aarch64";
return "aarch64";
}
if (userAgent.includes("intel") || userAgent.includes("x64") || userAgent.includes("x86_64")) {
if (userAgent.includes("x86_64") || userAgent.includes("amd64") || userAgent.includes("x64")) {
cachedMacArch = "x64";
return "x64";
}

try {
const canvas = document.createElement("canvas");
const gl = (canvas.getContext("webgl") || canvas.getContext("experimental-webgl")) as WebGLRenderingContext | null;
if (gl) {
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
if (debugInfo) {
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL).toLowerCase();
if (
renderer.includes("apple") ||
renderer.includes("m1") ||
renderer.includes("m2") ||
renderer.includes("m3") ||
renderer.includes("m4")
) {
return "aarch64";
}
}
}
} catch (e) {
// Ignore fallback
}


// If no explicit, trustworthy architecture signal is found, fail closed and return null
cachedMacArch = null;
return null;
};

Expand All @@ -111,6 +107,10 @@ export const getReleaseDownloads = (version: string) => ({
name: "Debian Package (.deb)",
url: getDownloadLink(version, ASSET_NAMES.linux.deb(version)),
},
{
name: "Red Hat Package (.rpm)",
url: getDownloadLink(version, ASSET_NAMES.linux.rpm(version)),
},
{
name: "Snap Store",
url: SNAP_STORE_URL,
Expand All @@ -128,6 +128,35 @@ export const getReleaseDownloads = (version: string) => ({
],
});

const detectLinuxDistro = (): "debian" | "redhat" | "generic" => {
if (typeof window === "undefined") return "generic";
const userAgent = window.navigator.userAgent.toLowerCase();

if (
userAgent.includes("ubuntu") ||
userAgent.includes("debian") ||
userAgent.includes("mint") ||
userAgent.includes("pop!_os") ||
userAgent.includes("elementary")
) {
return "debian";
}

if (
userAgent.includes("fedora") ||
userAgent.includes("red hat") ||
userAgent.includes("redhat") ||
userAgent.includes("centos") ||
userAgent.includes("suse") ||
userAgent.includes("rocky") ||
userAgent.includes("alma")
) {
return "redhat";
}

return "generic";
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export const getBestDownloadForOS = (os: string, version: string) => {
const tagUrl = `${GITHUB_RELEASES_URL}/tag/v${version}`;
switch (os) {
Expand All @@ -154,11 +183,26 @@ export const getBestDownloadForOS = (os: string, version: string) => {
url: tagUrl,
};
}
case "Linux":
case "Linux": {
const distro = detectLinuxDistro();
if (distro === "redhat") {
return {
label: "Download for Linux (.rpm)",
url: getDownloadLink(version, ASSET_NAMES.linux.rpm(version)),
};
}
if (distro === "debian") {
return {
label: "Download for Linux (.deb)",
url: getDownloadLink(version, ASSET_NAMES.linux.deb(version)),
};
}
// Safe generic Linux fallback (AppImage) that does not assume a package format
return {
label: "Download for Linux (.deb)",
url: getDownloadLink(version, ASSET_NAMES.linux.deb(version)),
label: "Download for Linux (AppImage)",
url: getDownloadLink(version, ASSET_NAMES.linux.appImage(version)),
};
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
default:
return {
label: "View All Downloads",
Expand Down
47 changes: 33 additions & 14 deletions src/pages/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,20 +166,39 @@ export function LandingPage() {
<div className="mb-4">{platform.icon}</div>
<h4 className="text-2xl font-bold mb-4">{platform.os}</h4>
<div className="space-y-2">
{platform.downloads.map((dl) => (
<a
key={dl.name}
href={dl.url}
target="_blank"
rel="noopener noreferrer"
className="block p-3 rounded-lg bg-surface border border-border hover:border-primary/50 hover:bg-primary/5 transition"
>
<div className="flex items-center justify-between">
<span className="font-semibold text-sm text-text-primary">{dl.name}</span>
<Download className="w-4 h-4 text-primary" />
</div>
</a>
))}
{platform.downloads.map((dl) => {
if (dl.name === "Snap Store") {
return (
<a
key={dl.name}
href={dl.url}
target="_blank"
rel="noopener noreferrer"
className="block w-full mt-2 hover:opacity-90 transition-opacity"
>
<img
alt="Get it from the Snap Store"
src="https://snapcraft.io/en/dark/install.svg"
className="w-full h-auto"
/>
</a>
);
}
return (
<a
key={dl.name}
href={dl.url}
target="_blank"
rel="noopener noreferrer"
className="block p-3 rounded-lg bg-surface border border-border hover:border-primary/50 hover:bg-primary/5 transition"
>
<div className="flex items-center justify-between">
<span className="font-semibold text-sm text-text-primary">{dl.name}</span>
<Download className="w-4 h-4 text-primary" />
</div>
</a>
);
})}
</div>
</div>
))}
Expand Down
3 changes: 3 additions & 0 deletions teebot_flow_landing_premium.html
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ <h1 class="hero-title">Business management, built for speed</h1>
<div><div class="dl-name">RPM Package</div><div class="dl-size">~6.5 MB</div></div>
<svg class="dl-arrow" viewBox="0 0 20 20"><path d="M10 3v10M6 9l4 4 4-4" stroke-linecap="round"/></svg>
</a>
<a href="https://snapcraft.io/teebot-flow" style="display: block; margin-top: 12px; width: 100%;" target="_blank" rel="noopener noreferrer">
<img alt="Get it from the Snap Store" src="https://snapcraft.io/en/dark/install.svg" style="width: 100%; height: auto; display: block;" />
</a>
</div>
<div class="platform-card">
<div class="platform-header">
Expand Down
Loading