feat: add download page

Signed-off-by: sisungo <[email protected]>
This commit is contained in:
2026-08-18 13:27:50 +08:00
parent 1d3c49ef84
commit fa9daab4a5
29 changed files with 1034 additions and 218 deletions
+31 -4
View File
@@ -1,11 +1,18 @@
{ {
"$schema": "https://inlang.com/schema/inlang-message-format", "$schema": "https://inlang.com/schema/inlang-message-format",
"nav_homepage": "Homepage", "nav_homepage": "Homepage",
"nav_download": "Download",
"nav_packages": "Packages", "nav_packages": "Packages",
"nav_wiki": "Wiki",
"foot_social": "Social",
"homepage_slogan": "Modern, fast and secure operating system.",
"homepage_slogan_detailed": "SemiOS is a general purposed operating system with redesigned system components that strive to be fast, secure and flexible.\n\nIt is available for a variety of platforms, from desktop PCs to embedded boards.",
"homepage_get": "Get SemiOS",
"packages_search_title": "Search Packages", "packages_search_title": "Search Packages",
"packages_mirror_select": "Select an Mirror", "packages_mirror_select": "Select an Mirror",
"packages_repo_select": "Select an Repository",
"packages_search_keyword": "Keyword", "packages_search_keyword": "Keyword",
"packages_arch_select": "Architecture", "packages_arch_select": "Filtering Architecture",
"packages_show_latest_only": "Show latest only", "packages_show_latest_only": "Show latest only",
"packages_search_action": "Search", "packages_search_action": "Search",
"packages_name": "Package Name", "packages_name": "Package Name",
@@ -14,16 +21,36 @@
"pkginfo_version": "Version", "pkginfo_version": "Version",
"pkginfo_arch": "Architecture", "pkginfo_arch": "Architecture",
"pkginfo_installed_size": "Installed Size", "pkginfo_installed_size": "Installed Size",
"pkginfo_install_title": "Install Package",
"pkginfo_install_download": "Download File",
"pkginfo_install_packie_prompt_terminal": "To install this package via Packie in the default environment, execute the following command in the terminal:",
"pkginfo_install_packie_pasted": "Command pasted!",
"pkginfo_download_size": "Download Size", "pkginfo_download_size": "Download Size",
"pkginfo_download_title": "Download Package",
"pkginfo_download_action": "Download", "pkginfo_download_action": "Download",
"pkginfo_download_filename": "Download Filename", "pkginfo_download_filename": "Download Filename",
"pkginfo_download_checksum": "File Checksum",
"pkginfo_dependency_title": "Dependencies", "pkginfo_dependency_title": "Dependencies",
"pkginfo_dependency_description": "This will list the dependencies and recommended packages for this package.", "pkginfo_dependency_description": "This will list the dependencies and recommended packages for this package.",
"pkginfo_dependency_pkgspec": "Package Specification", "pkginfo_dependency_pkgspec": "Package Specification",
"pkginfo_dependency_type": "Type", "pkginfo_dependency_type": "Type",
"pkginfo_dependency_type_required": "Required", "pkginfo_dependency_type_required": "Required",
"pkginfo_dependency_type_recommended": "Recommended", "pkginfo_dependency_type_recommended": "Recommended",
"homepage_slogan": "The innovative operating system for the next generation of computing.", "pkginfo_maintainers": "Maintainers",
"homepage_get": "Get SemiOS" "download_iso_image": "Get ISO Image",
"download_iso_image_description": "Select and get the latest LiveCD images for general devices, like workstations and servers.",
"download_iso_arch": "Architecture",
"download_iso_image_ty": "Image Type",
"download_iso_manual": "Wondering how to install it? Click ",
"download_now": "Download Now",
"download_mirrors_title": "Mirrors",
"download_mirrors_description": "Maybe choosing a mirror site manually speeds up your download.",
"download_mirrors_region": "Region",
"download_mirrors_server": "Server",
"download_mirrors_access": "Access",
"download_container_title": "Running SemiOS in a Container",
"download_container_direct_deploy": "Deploy Directly",
"download_container_arch": "Architecture",
"download_embedded_title": "Embedded Devices",
"download_embedded_text": "To install SemiOS on an embedded device (e.g. demo boards), view our wiki to see supported devices and flashing guides.",
"download_embedded_go": "Go to the Wiki Page"
} }
+30 -3
View File
@@ -1,9 +1,16 @@
{ {
"$schema": "https://inlang.com/schema/inlang-message-format", "$schema": "https://inlang.com/schema/inlang-message-format",
"nav_homepage": "ホームページ", "nav_homepage": "ホームページ",
"nav_download": "ダウンロード",
"nav_packages": "パッケージ", "nav_packages": "パッケージ",
"nav_wiki": "ウィキ",
"foot_social": "ソーシャル",
"homepage_slogan": "現代的で、高速かつ安全なオペレーティングシステム。",
"homepage_slogan_detailed": "SemiOSは、高速性・安全性・柔軟性を追求して再設計されたシステムコンポーネントを備えた、汎用オペレーティングシステムです。\n\nデスクトップPCから組み込みボードまで、多様なプラットフォームで利用可能です。",
"homepage_get": "SemiOS を入手",
"packages_search_title": "パッケージを検索", "packages_search_title": "パッケージを検索",
"packages_mirror_select": "ミラーを選択", "packages_mirror_select": "ミラーを選択",
"packages_repo_select": "リポジトリを選択",
"packages_search_keyword": "キーワード", "packages_search_keyword": "キーワード",
"packages_arch_select": "アーキテクチャ", "packages_arch_select": "アーキテクチャ",
"packages_show_latest_only": "最新のみ表示", "packages_show_latest_only": "最新のみ表示",
@@ -14,16 +21,36 @@
"pkginfo_version": "バージョン", "pkginfo_version": "バージョン",
"pkginfo_arch": "アーキテクチャ", "pkginfo_arch": "アーキテクチャ",
"pkginfo_installed_size": "インストールサイズ", "pkginfo_installed_size": "インストールサイズ",
"pkginfo_install_title": "インストール",
"pkginfo_install_download": "ファイルをダウンロード",
"pkginfo_install_packie_prompt_terminal": "このパッケージをPackieを使ってデフォルト環境にインストールするには、ターミナルで以下のコマンドを実行してください:",
"pkginfo_install_packie_pasted": "コマンドを貼り付けました!",
"pkginfo_download_size": "ダウンロードサイズ", "pkginfo_download_size": "ダウンロードサイズ",
"pkginfo_download_title": "パッケージをダウンロード",
"pkginfo_download_action": "ダウンロード", "pkginfo_download_action": "ダウンロード",
"pkginfo_download_filename": "ダウンロードファイル名", "pkginfo_download_filename": "ダウンロードファイル名",
"pkginfo_download_checksum": "チェックサム値",
"pkginfo_dependency_title": "依存関係", "pkginfo_dependency_title": "依存関係",
"pkginfo_dependency_description": "これにより、このパッケージの依存関係と推奨パッケージが一覧表示されます。", "pkginfo_dependency_description": "これにより、このパッケージの依存関係と推奨パッケージが一覧表示されます。",
"pkginfo_dependency_pkgspec": "パッケージ仕様", "pkginfo_dependency_pkgspec": "パッケージ仕様",
"pkginfo_dependency_type": "タイプ", "pkginfo_dependency_type": "タイプ",
"pkginfo_dependency_type_required": "必須", "pkginfo_dependency_type_required": "必須",
"pkginfo_dependency_type_recommended": "推奨", "pkginfo_dependency_type_recommended": "推奨",
"homepage_slogan": "次世代コンピューティングのための革新的なオペレーティングシステム。", "pkginfo_maintainers": "メンテナー",
"homepage_get": "SemiOS を入手" "download_iso_image": "ISO イメージを入手",
"download_iso_image_description": "ワークステーションやサーバーなどの一般デバイス向けの最新 LiveCD イメージを選択して入手してください。",
"download_iso_arch": "アーキテクチャ",
"download_iso_image_ty": "イメージタイプ",
"download_iso_manual": "インストール方法がわからない場合は、クリック ",
"download_now": "今すぐ入手",
"download_mirrors_title": "ミラー",
"download_mirrors_description": "手動でミラーサイトを選択すると、ダウンロードが高速化される場合があります。",
"download_mirrors_region": "リージョン",
"download_mirrors_server": "サーバー",
"download_mirrors_access": "アクセス",
"download_container_title": "コンテナでSemiOSを実行する",
"download_container_direct_deploy": "直接デプロイ",
"download_container_arch": "アーキテクチャ",
"download_embedded_title": "組み込みデバイス",
"download_embedded_text": "組み込みデバイス(例:デモボード)にSemiOSをインストールするには、ウィキを参照して、サポートされているデバイスとフラッシュ書き込みガイドをご確認ください。",
"download_embedded_go": "ウィキページへ進む"
} }
+31 -4
View File
@@ -1,11 +1,18 @@
{ {
"$schema": "https://inlang.com/schema/inlang-message-format", "$schema": "https://inlang.com/schema/inlang-message-format",
"nav_homepage": "首页", "nav_homepage": "首页",
"nav_download": "下载",
"nav_packages": "软件包", "nav_packages": "软件包",
"nav_wiki": "文档",
"foot_social": "社交媒体",
"homepage_slogan": "现代、快速、安全的操作系统。",
"homepage_slogan_detailed": "SemiOS 是一款通用操作系统,其系统组件经过重新设计,力求实现高速、安全与灵活。\n\n从桌面电脑到嵌入式开发板,它支持多种平台。",
"homepage_get": "获取 SemiOS",
"packages_search_title": "搜索软件包", "packages_search_title": "搜索软件包",
"packages_mirror_select": "选择镜像站", "packages_mirror_select": "选择镜像站",
"packages_repo_select": "选择仓库",
"packages_search_keyword": "关键字", "packages_search_keyword": "关键字",
"packages_arch_select": "架构", "packages_arch_select": "筛选架构",
"packages_show_latest_only": "仅显示最新版", "packages_show_latest_only": "仅显示最新版",
"packages_search_action": "搜索", "packages_search_action": "搜索",
"packages_name": "软件包名称", "packages_name": "软件包名称",
@@ -14,16 +21,36 @@
"pkginfo_version": "版本", "pkginfo_version": "版本",
"pkginfo_arch": "架构", "pkginfo_arch": "架构",
"pkginfo_installed_size": "安装大小", "pkginfo_installed_size": "安装大小",
"pkginfo_install_title": "安装软件包",
"pkginfo_install_download": "下载文件",
"pkginfo_install_packie_prompt_terminal": "要通过Packie在默认环境中安装此软件包,请在终端中执行以下命令:",
"pkginfo_install_packie_pasted": "内容已复制!",
"pkginfo_download_size": "下载大小", "pkginfo_download_size": "下载大小",
"pkginfo_download_title": "下载软件包",
"pkginfo_download_action": "立即下载", "pkginfo_download_action": "立即下载",
"pkginfo_download_filename": "下载文件名", "pkginfo_download_filename": "下载文件名",
"pkginfo_download_checksum": "文件校验码",
"pkginfo_dependency_title": "依赖关系", "pkginfo_dependency_title": "依赖关系",
"pkginfo_dependency_description": "这里将会列举出此包依赖和建议安装的包。", "pkginfo_dependency_description": "这里将会列举出此包依赖和建议安装的包。",
"pkginfo_dependency_pkgspec": "软件包", "pkginfo_dependency_pkgspec": "软件包",
"pkginfo_dependency_type": "类型", "pkginfo_dependency_type": "类型",
"pkginfo_dependency_type_required": "必需", "pkginfo_dependency_type_required": "必需",
"pkginfo_dependency_type_recommended": "建议", "pkginfo_dependency_type_recommended": "建议",
"homepage_slogan": "面向下一代计算的创新操作系统。", "pkginfo_maintainers": "维护者",
"homepage_get": "获取 SemiOS" "download_iso_image": "获取 ISO 镜像",
"download_iso_image_description": "选择并获取适用于工作站和服务器等通用设备的最新版本 LiveCD 镜像。",
"download_iso_arch": "架构",
"download_iso_image_ty": "镜像类型",
"download_iso_manual": "想要获取安装指南?点击 ",
"download_now": "立即下载",
"download_mirrors_title": "镜像站",
"download_mirrors_description": "有时手动选择一个镜像站可能提高下载软件的速度。",
"download_mirrors_region": "地区",
"download_mirrors_server": "服务器",
"download_mirrors_access": "访问镜像站",
"download_container_title": "在容器中运行 SemiOS",
"download_container_direct_deploy": "直接部署",
"download_container_arch": "架构",
"download_embedded_title": "嵌入式设备",
"download_embedded_text": "要在嵌入式设备(例如演示板)上安装SemiOS,请查看我们的维基页面,了解支持的设备和烧录指南。",
"download_embedded_go": "前往维基页面"
} }
+35 -4
View File
@@ -17,7 +17,7 @@
"@fontsource-variable/inter": "^5.3.0", "@fontsource-variable/inter": "^5.3.0",
"@inlang/paraglide-js": "^2.18.2", "@inlang/paraglide-js": "^2.18.2",
"@internationalized/date": "^3.12.3", "@internationalized/date": "^3.12.3",
"@lucide/svelte": "^1.29.0", "@lucide/svelte": "^1.31.0",
"@storybook/addon-a11y": "^10.5.7", "@storybook/addon-a11y": "^10.5.7",
"@storybook/addon-docs": "^10.5.7", "@storybook/addon-docs": "^10.5.7",
"@storybook/addon-svelte-csf": "latest", "@storybook/addon-svelte-csf": "latest",
@@ -49,6 +49,7 @@
"storybook": "^10.5.7", "storybook": "^10.5.7",
"svelte": "^5.56.1", "svelte": "^5.56.1",
"svelte-check": "^4.6.0", "svelte-check": "^4.6.0",
"svelte-sonner": "^1.2.1",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"tailwind-variants": "^3.3.1", "tailwind-variants": "^3.3.1",
"tailwindcss": "^4.3.0", "tailwindcss": "^4.3.0",
@@ -1033,9 +1034,9 @@
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@lucide/svelte": { "node_modules/@lucide/svelte": {
"version": "1.29.0", "version": "1.31.0",
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.29.0.tgz", "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.31.0.tgz",
"integrity": "sha512-qzpfzxG8WZiAnyjh1qaQCtG4MA5R6HOyvb3c+LezexgFf69ptBdK3PjvGYGCWHKgTIVZMsOU9fqfczjsZAJl0A==", "integrity": "sha512-pgPuLEJdF0UpsajG9eSw0YvqXVuDxB+/x58BRiC4xE/KEHiopOqT1id3KCUvRN/PUmQkN1F26sslcs/1jutlBQ==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peerDependencies": { "peerDependencies": {
@@ -6558,6 +6559,36 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/svelte-sonner": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.2.1.tgz",
"integrity": "sha512-N00LOWb60fjKXULbpngku+7E9Krb16KpNq4/Eax5Vu7o1Dpbca55QpaLPsB+yp82JIEdFhfJuv5cVXLoe0ZtVA==",
"dev": true,
"license": "MIT",
"dependencies": {
"runed": "^0.28.0"
},
"peerDependencies": {
"svelte": "^5.0.0"
}
},
"node_modules/svelte-sonner/node_modules/runed": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/runed/-/runed-0.28.0.tgz",
"integrity": "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==",
"dev": true,
"funding": [
"https://github.com/sponsors/huntabyte",
"https://github.com/sponsors/tglide"
],
"license": "MIT",
"dependencies": {
"esm-env": "^1.0.0"
},
"peerDependencies": {
"svelte": "^5.7.0"
}
},
"node_modules/svelte-toolbelt": { "node_modules/svelte-toolbelt": {
"version": "0.7.1", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz", "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz",
+2 -1
View File
@@ -21,7 +21,7 @@
"@fontsource-variable/inter": "^5.3.0", "@fontsource-variable/inter": "^5.3.0",
"@inlang/paraglide-js": "^2.18.2", "@inlang/paraglide-js": "^2.18.2",
"@internationalized/date": "^3.12.3", "@internationalized/date": "^3.12.3",
"@lucide/svelte": "^1.29.0", "@lucide/svelte": "^1.31.0",
"@storybook/addon-a11y": "^10.5.7", "@storybook/addon-a11y": "^10.5.7",
"@storybook/addon-docs": "^10.5.7", "@storybook/addon-docs": "^10.5.7",
"@storybook/addon-svelte-csf": "latest", "@storybook/addon-svelte-csf": "latest",
@@ -53,6 +53,7 @@
"storybook": "^10.5.7", "storybook": "^10.5.7",
"svelte": "^5.56.1", "svelte": "^5.56.1",
"svelte-check": "^4.6.0", "svelte-check": "^4.6.0",
"svelte-sonner": "^1.2.1",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"tailwind-variants": "^3.3.1", "tailwind-variants": "^3.3.1",
"tailwindcss": "^4.3.0", "tailwindcss": "^4.3.0",
+27
View File
@@ -0,0 +1,27 @@
<script lang="ts">
import Button from '$lib/components/ui/button/button.svelte';
import Separator from '$lib/components/ui/separator/separator.svelte';
import { GitBranch } from '@lucide/svelte';
import { resolve } from '$app/paths';
import * as m from '$lib/paraglide/messages';
</script>
<Separator class="mt-6" />
<div class="mt-4 mb-4 flex flex-col justify-between md:flex-row">
<div
class="flex flex-col items-center gap-1 text-center text-sm md:ml-6 md:items-start md:text-start"
>
<span>Copyright 2026 SemiOS Developers</span>
<a href={resolve('/policy/privacy')} class="underline"> Privacy Policy </a>
</div>
<div class="flex items-center justify-center gap-2 md:mr-6">
<span class="text-sm">{m.foot_social()}:</span>
<a href="https://gitea.semilabs.org/semios">
<Button size="icon" variant="ghost">
<GitBranch />
</Button>
</a>
</div>
</div>
+14 -1
View File
@@ -3,6 +3,7 @@
import { Label } from "$lib/components/ui/label/index.js"; import { Label } from "$lib/components/ui/label/index.js";
import * as m from "$lib/paraglide/messages"; import * as m from "$lib/paraglide/messages";
import { resolve } from '$app/paths';
</script> </script>
<div class="flex max-w-6xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8"> <div class="flex max-w-6xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
@@ -16,10 +17,22 @@
</NavigationMenu.Item> </NavigationMenu.Item>
<NavigationMenu.Item> <NavigationMenu.Item>
<NavigationMenu.Link href="/packages.html" class="text-sm font-medium"> <NavigationMenu.Link href={resolve("/packages")} class="text-sm font-medium">
{m.nav_packages()} {m.nav_packages()}
</NavigationMenu.Link> </NavigationMenu.Link>
</NavigationMenu.Item> </NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Link href="/download" class="text-sm font-medium">
{m.nav_download()}
</NavigationMenu.Link>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Link href="https://wiki.os.semilabs.org" class="text-sm font-medium">
{m.nav_wiki()}
</NavigationMenu.Link>
</NavigationMenu.Item>
</NavigationMenu.List> </NavigationMenu.List>
</NavigationMenu.Root> </NavigationMenu.Root>
</div> </div>
@@ -0,0 +1,26 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="avatar-badge"
class={cn(
"bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none",
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
className
)}
{...restProps}
>
{@render children?.()}
</span>
@@ -0,0 +1,20 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.FallbackProps = $props();
</script>
<AvatarPrimitive.Fallback
bind:ref
data-slot="avatar-fallback"
class={cn(
"rounded-full bg-muted text-muted-foreground flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs",
className
)}
{...restProps}
/>
@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="avatar-group-count"
class={cn(
"size-8 rounded-full bg-muted text-sm text-muted-foreground group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 relative flex shrink-0 items-center justify-center ring-2 ring-background",
className
)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="avatar-group"
class={cn(
"cn-avatar-group group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
className
)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,17 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.ImageProps = $props();
</script>
<AvatarPrimitive.Image
bind:ref
data-slot="avatar-image"
class={cn("rounded-full aspect-square size-full object-cover", className)}
{...restProps}
/>
@@ -0,0 +1,26 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
loadingStatus = $bindable("loading"),
size = "default",
class: className,
...restProps
}: AvatarPrimitive.RootProps & {
size?: "default" | "sm" | "lg";
} = $props();
</script>
<AvatarPrimitive.Root
bind:ref
bind:loadingStatus
data-slot="avatar"
data-size={size}
class={cn(
"size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:border-border after:mix-blend-darken dark:after:mix-blend-lighten",
className
)}
{...restProps}
/>
+22
View File
@@ -0,0 +1,22 @@
import Badge from "./avatar-badge.svelte";
import Fallback from "./avatar-fallback.svelte";
import GroupCount from "./avatar-group-count.svelte";
import Group from "./avatar-group.svelte";
import Image from "./avatar-image.svelte";
import Root from "./avatar.svelte";
export {
Root,
Image,
Fallback,
Badge,
Group,
GroupCount,
//
Root as Avatar,
Image as AvatarImage,
Fallback as AvatarFallback,
Badge as AvatarBadge,
Group as AvatarGroup,
GroupCount as AvatarGroupCount,
};
+1
View File
@@ -0,0 +1 @@
export { default as Toaster } from "./sonner.svelte";
@@ -0,0 +1,34 @@
<script lang="ts">
import { mode } from "mode-watcher";
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
import Loader2Icon from '@lucide/svelte/icons/loader-2';
import CircleCheckIcon from '@lucide/svelte/icons/circle-check';
import OctagonXIcon from '@lucide/svelte/icons/octagon-x';
import InfoIcon from '@lucide/svelte/icons/info';
import TriangleAlertIcon from '@lucide/svelte/icons/triangle-alert';
let { ...restProps }: SonnerProps = $props();
</script>
<Sonner
theme={mode.current}
class="toaster group"
style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);"
{...restProps}
>
{#snippet loadingIcon()}
<Loader2Icon class="size-4 animate-spin" />
{/snippet}
{#snippet successIcon()}
<CircleCheckIcon class="size-4" />
{/snippet}
{#snippet errorIcon()}
<OctagonXIcon class="size-4" />
{/snippet}
{#snippet infoIcon()}
<InfoIcon class="size-4" />
{/snippet}
{#snippet warningIcon()}
<TriangleAlertIcon class="size-4" />
{/snippet}
</Sonner>
+18
View File
@@ -0,0 +1,18 @@
import Content from "./tabs-content.svelte";
import Trigger from "./tabs-trigger.svelte";
import Root from "./tabs.svelte";
import List, { tabsListVariants, type TabsListVariant } from "./tabs-list.svelte";
export {
Root,
Content,
List,
Trigger,
tabsListVariants,
type TabsListVariant,
//
Root as Tabs,
Content as TabsContent,
List as TabsList,
Trigger as TabsTrigger,
};
@@ -0,0 +1,17 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: TabsPrimitive.ContentProps = $props();
</script>
<TabsPrimitive.Content
bind:ref
data-slot="tabs-content"
class={cn("text-sm flex-1 outline-none", className)}
{...restProps}
/>
@@ -0,0 +1,40 @@
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";
export const tabsListVariants = tv({
base: "rounded-full p-1 group-data-horizontal/tabs:h-9 group-data-vertical/tabs:rounded-2xl data-[variant=line]:rounded-none group/tabs-list inline-flex w-fit items-center justify-center text-muted-foreground group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
variants: {
variant: {
default: "cn-tabs-list-variant-default bg-muted",
line: "cn-tabs-list-variant-line gap-1 bg-transparent",
},
},
defaultVariants: {
variant: "default",
},
});
export type TabsListVariant = VariantProps<typeof tabsListVariants>["variant"];
</script>
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
variant = "default",
class: className,
...restProps
}: TabsPrimitive.ListProps & {
variant?: TabsListVariant;
} = $props();
</script>
<TabsPrimitive.List
bind:ref
data-slot="tabs-list"
data-variant={variant}
class={cn(tabsListVariants({ variant }), className)}
{...restProps}
/>
@@ -0,0 +1,23 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: TabsPrimitive.TriggerProps = $props();
</script>
<TabsPrimitive.Trigger
bind:ref
data-slot="tabs-trigger"
class={cn(
"gap-2 rounded-full border border-transparent! px-3 py-1 text-sm font-medium group-data-vertical/tabs:rounded-2xl group-data-vertical/tabs:px-3 group-data-vertical/tabs:py-1.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-4 relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
className
)}
{...restProps}
/>
+19
View File
@@ -0,0 +1,19 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(""),
class: className,
...restProps
}: TabsPrimitive.RootProps = $props();
</script>
<TabsPrimitive.Root
bind:ref
bind:value
data-slot="tabs"
class={cn("gap-2 group/tabs flex data-[orientation=horizontal]:flex-col", className)}
{...restProps}
/>
+15
View File
@@ -0,0 +1,15 @@
export const MIRRORS: Record<string, string[]> = {
"Global": [
"mirror1.semilabs.org",
],
"East Asia": [
"mirror2.semilabs.org",
],
};
export const MAINTAINERS: Record<string, { name: string, avatar: string }> = {
"sisungo <[email protected]>": {
"name": "sisungo",
"avatar": "https://github.com/sisungo.png",
},
}
+7 -2
View File
@@ -6,12 +6,15 @@
import './layout.css'; import './layout.css';
import { ModeWatcher } from 'mode-watcher'; import { ModeWatcher } from 'mode-watcher';
import favicon from '$lib/assets/favicon.svg'; import favicon from '$lib/assets/favicon.svg';
import Topbar from '../components/Topbar.svelte';
import { setLocale } from "$lib/paraglide/runtime"; import { setLocale } from "$lib/paraglide/runtime";
import { Toaster } from "$lib/components/ui/sonner/index.js";
import Topbar from '../components/Topbar.svelte';
import Pagefoot from '../components/Pagefoot.svelte';
let { children } = $props(); let { children } = $props();
function normalizeLanguage(lang) { function normalizeLanguage(lang: string): 'zh-cn' | 'ja' | 'en' | null {
if (!lang) return null; if (!lang) return null;
lang = lang.toLowerCase(); lang = lang.toLowerCase();
@@ -28,8 +31,10 @@
<svelte:head><link rel="icon" href={favicon} /></svelte:head> <svelte:head><link rel="icon" href={favicon} /></svelte:head>
<ModeWatcher /> <ModeWatcher />
<Toaster />
<Topbar /> <Topbar />
{@render children()} {@render children()}
<Pagefoot />
<div style="display:none"> <div style="display:none">
{#each locales as locale (locale)} {#each locales as locale (locale)}
+8 -4
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import * as m from "$lib/paraglide/messages"; import * as m from "$lib/paraglide/messages";
import { resolve } from "$app/paths";
import Button from "$lib/components/ui/button/button.svelte"; import Button from "$lib/components/ui/button/button.svelte";
</script> </script>
@@ -9,9 +10,12 @@
<h1 class="text-4xl font-bold"> <h1 class="text-4xl font-bold">
{m.homepage_slogan()} {m.homepage_slogan()}
</h1> </h1>
<Button class="mt-4 flex"> <p class="mt-2 text-muted-foreground whitespace-pre-line">{m.homepage_slogan_detailed()}</p>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-download-icon lucide-download"><path d="M12 15V3"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/></svg> <a href={resolve("/download")}>
<p>{m.homepage_get()}</p> <Button class="mt-6 flex">
</Button> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-download-icon lucide-download"><path d="M12 15V3"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/></svg>
<p>{m.homepage_get()}</p>
</Button>
</a>
</div> </div>
</div> </div>
+174
View File
@@ -0,0 +1,174 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import Label from '$lib/components/ui/label/label.svelte';
import * as Select from '$lib/components/ui/select/index.js';
import * as Tabs from '$lib/components/ui/tabs/index.js';
import { Archive, CircleArrowRight, Container, Cpu, Disc, Download } from '@lucide/svelte';
import * as m from '$lib/paraglide/messages';
import { MIRRORS } from '$lib/mirrors';
let isoImageArch = $state('x86_64');
let isoImageTy = $state('gui');
let mirrorRegion = $state('Global');
let mirrorServer = $state('mirror1.semilabs.org');
let containerArch = $state('x86_64-semios-linux');
</script>
<div class="mr-4 ml-4">
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
<Disc />
{m.download_iso_image()}
</Card.Title>
<Card.Description>{m.download_iso_image_description()}</Card.Description>
</Card.Header>
<Card.Content>
<div class="flex gap-4 flex-col md:flex-row">
<div>
<Label class="mb-2 text-sm">{m.download_iso_arch()}</Label>
<Select.Root type="single" bind:value={isoImageArch}>
<Select.Trigger class="w-[180px]">{isoImageArch}</Select.Trigger>
<Select.Content>
<Select.Item value="x86_64">x86_64</Select.Item>
<Select.Item value="aarch64">aarch64</Select.Item>
<Select.Item value="riscv64">riscv64</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div>
<Label class="mb-2 text-sm">{m.download_iso_image_ty()}</Label>
<Select.Root type="single" bind:value={isoImageTy}>
<Select.Trigger class="w-[180px]">{isoImageTy}</Select.Trigger>
<Select.Content>
<Select.Item value="netboot">NetBoot</Select.Item>
<Select.Item value="minimal">Minimal LiveCD</Select.Item>
<Select.Item value="gui">GUI LiveCD</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
<div class="mt-6">
{m.download_iso_manual()}
<a href="https://wiki.os.semilabs.org/Installation_Guide" class="underline">Installation Guide</a>
</div>
</Card.Content>
<Card.Footer>
<Button variant="default">
<Download />
{m.download_now()}
</Button>
</Card.Footer>
</Card.Root>
<div class="mt-4 flex flex-col gap-4">
<div class="flex flex-col gap-4 md:flex-row">
<Card.Root class="w-full">
<Card.Header>
<Card.Title class="flex items-center gap-2">
<Container />
{m.download_container_title()}
</Card.Title>
</Card.Header>
<Card.Content>
<Tabs.Root value="direct_deploy" class="w-[400px]">
<Tabs.List>
<Tabs.Trigger value="direct_deploy"
>{m.download_container_direct_deploy()}</Tabs.Trigger
>
<Tabs.Trigger value="docker">Docker</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="direct_deploy">
<div>
<Label class="mb-2 text-sm">{m.download_container_arch()}</Label>
<Select.Root type="single" bind:value={containerArch}>
<Select.Trigger class="w-[180px]">{containerArch}</Select.Trigger>
<Select.Content>
<Select.Item value="x86_64-semios-linux">x86_64-semios-linux</Select.Item>
<Select.Item value="aarch64-semios-linux">aarch64-semios-linux</Select.Item>
<Select.Item value="riscv64-semios-linux">riscv64-semios-linux</Select.Item>
</Select.Content>
</Select.Root>
</div>
<Button class="mt-4">
<Download />
{m.download_now()}
</Button>
</Tabs.Content>
<Tabs.Content value="docker">Not yet available.</Tabs.Content>
</Tabs.Root>
</Card.Content>
</Card.Root>
<Card.Root class="w-full">
<Card.Header>
<Card.Title class="flex items-center gap-2">
<Archive />
{m.download_mirrors_title()}
</Card.Title>
<Card.Description>{m.download_mirrors_description()}</Card.Description>
</Card.Header>
<Card.Content>
<div class="flex flex-col gap-2 md:flex-row">
<div>
<Label class="mb-2 text-sm">{m.download_mirrors_region()}</Label>
<Select.Root type="single" bind:value={mirrorRegion}>
<Select.Trigger class="w-[180px]">{mirrorRegion}</Select.Trigger>
<Select.Content>
{#each Object.keys(MIRRORS) as region (region)}
<Select.Item
value={region}
onclick={() => {
mirrorServer = MIRRORS[region][0];
}}>{region}</Select.Item
>
{/each}
</Select.Content>
</Select.Root>
</div>
<div>
<Label class="mb-2 text-sm">{m.download_mirrors_server()}</Label>
<Select.Root type="single" bind:value={mirrorServer}>
<Select.Trigger class="w-[180px]">{mirrorServer}</Select.Trigger>
<Select.Content>
{#each MIRRORS[mirrorRegion] as server (server)}
<Select.Item value={server}>{server}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
</Card.Content>
<Card.Footer>
<a href={`https://${mirrorServer}/semios/images`}>
<Button>
<CircleArrowRight />
{m.download_mirrors_access()}
</Button>
</a>
</Card.Footer>
</Card.Root>
</div>
<div class="flex flex-col gap-4 md:flex-row">
<Card.Root class="w-full">
<Card.Header>
<Card.Title class="flex items-center gap-2">
<Cpu /> {m.download_embedded_title()}
</Card.Title>
</Card.Header>
<Card.Content>
<p>{m.download_embedded_text()}</p>
</Card.Content>
<Card.Footer>
<a href="http://wiki.os.semilabs.org/Embedded_Devices" class="underline">{m.download_embedded_go()}</a>
</Card.Footer>
</Card.Root>
</div>
</div>
</div>
+141 -88
View File
@@ -1,96 +1,149 @@
<script lang="ts"> <script lang="ts">
import { Button } from "$lib/components/ui/button/index.js"; import { Button } from '$lib/components/ui/button/index.js';
import * as Card from "$lib/components/ui/card/index.js"; import * as Card from '$lib/components/ui/card/index.js';
import Checkbox from "$lib/components/ui/checkbox/checkbox.svelte"; import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
import { Input } from "$lib/components/ui/input/index.js"; import { Input } from '$lib/components/ui/input/index.js';
import * as Select from "$lib/components/ui/select/index.js"; import * as Select from '$lib/components/ui/select/index.js';
import * as Table from "$lib/components/ui/table/index.js"; import * as Table from '$lib/components/ui/table/index.js';
import { SvelteMap } from "svelte/reactivity"; import { SvelteMap } from 'svelte/reactivity';
import * as m from "$lib/paraglide/messages"; import * as m from '$lib/paraglide/messages';
import { resolve } from '$app/paths';
import { getRepoDb, searchPackages, type PkgIdent, baseUrlForMirror, strPkgIdent } from "./helper.ts"; import {
getRepoDb,
searchPackages,
type PkgIdent,
baseUrlForMirror,
strPkgIdent
} from './helper.ts';
import Label from '$lib/components/ui/label/label.svelte';
import { Axe, Server } from '@lucide/svelte';
let showLatestOnly = $state(false); let showLatestOnly = $state(false);
let filterArchitecture = $state(""); let filterArchitecture = $state('');
let selectedMirror = $state("mirror1.semilabs.org"); let selectedMirror = $state('mirror1.semilabs.org');
let searchKeyword = $state(""); let selectedRepo = $state('official');
let results = $state<Array<PkgIdent>>([]); let searchKeyword = $state('');
let results = $state<Array<PkgIdent>>([]);
async function search() { async function search() {
const baseUrl = baseUrlForMirror(selectedMirror); const baseUrl = baseUrlForMirror(selectedMirror);
const repo = await getRepoDb(`${baseUrl}/repo.db`); const repo = await getRepoDb(`${baseUrl}/repo.db`);
results = searchPackages(repo, searchKeyword); results = searchPackages(repo, searchKeyword);
if (filterArchitecture) { if (filterArchitecture) {
results = results.filter((item) => item.arch === filterArchitecture || item.arch === "any"); results = results.filter((item) => item.arch === filterArchitecture || item.arch === 'any');
} }
if (showLatestOnly) { if (showLatestOnly) {
const latestResults = new SvelteMap<string, PkgIdent>(); const latestResults = new SvelteMap<string, PkgIdent>();
for (const item of results) { for (const item of results) {
const existing = latestResults.get(item.name); const existing = latestResults.get(item.name);
if (!existing || item.version > existing.version) { if (!existing || item.version > existing.version) {
latestResults.set(item.name, item); latestResults.set(item.name, item);
} }
} }
results = Array.from(latestResults.values()); results = Array.from(latestResults.values());
} }
} }
function pkgInfoUrl(mirrorName: string, pkgIdent: PkgIdent): string {
const params = {
mirrorName,
pkgIdent: strPkgIdent(pkgIdent),
};
const searchParams = new URLSearchParams(params);
return resolve(`/packages/pkginfo?${searchParams}`);
}
</script> </script>
<div class="ml-4 mr-4"> <div class="mr-4 ml-4">
<Card.Root> <div class="flex">
<Card.Header> <Card.Root class="w-full">
<Card.Title>{m.packages_search_title()}</Card.Title> <Card.Header>
</Card.Header> <Card.Title class="flex w-full items-center justify-between">
<Card.Content class="flex flex-col gap-4"> <Label class="text-xl font-bold">{m.packages_search_title()}</Label>
<Select.Root type="single" bind:value={selectedMirror}> <div class="flex gap-2">
<Select.Trigger class="w-[180px]">{m.packages_mirror_select()}</Select.Trigger> <Button variant="outline" size="icon" class="float-end">
<Select.Content> <Server />
<Select.Item value="mirror1.semilabs.org">mirror1.semilabs.org</Select.Item> </Button>
<Select.Item value="mirror2.semilabs.org">mirror2.semilabs.org</Select.Item> <Button variant="outline" size="icon" class="float-end">
</Select.Content> <Axe />
</Select.Root> </Button>
<Input placeholder={m.packages_search_keyword()} bind:value={searchKeyword} class="w-1/2" /> </div>
<div class="flex items-center gap-4"> </Card.Title>
<Select.Root type="single" bind:value={filterArchitecture}> </Card.Header>
<Select.Trigger class="w-[180px]">{m.packages_arch_select()}</Select.Trigger> <Card.Content class="flex flex-col gap-4">
<Select.Content> <div class="flex flex-col md:flex-row gap-4">
<Select.Item value="aarch64-semios-linux">aarch64-semios-linux</Select.Item> <div>
<Select.Item value="x86_64-semios-linux">x86_64-semios-linux</Select.Item> <Label class="text-sm">{m.packages_mirror_select()}</Label>
</Select.Content> <Select.Root type="single" bind:value={selectedMirror}>
</Select.Root> <Select.Trigger class="mt-1 w-[180px]">{selectedMirror}</Select.Trigger>
<div class="flex items-center gap-2"> <Select.Content>
<Checkbox bind:checked={showLatestOnly} /> <Select.Item value="mirror1.semilabs.org">mirror1.semilabs.org</Select.Item>
<span>{m.packages_show_latest_only()}</span> <Select.Item value="mirror2.semilabs.org">mirror2.semilabs.org</Select.Item>
</div> </Select.Content>
</div> </Select.Root>
</Card.Content> </div>
<Card.Footer> <div>
<Button variant="default" onclick={search}> <Label class="text-sm">{m.packages_repo_select()}</Label>
{m.packages_search_action()} <Select.Root type="single" bind:value={selectedRepo}>
</Button> <Select.Trigger class="mt-1 w-[180px]">{selectedRepo}</Select.Trigger>
</Card.Footer> <Select.Content>
</Card.Root> <Select.Item value="official">official</Select.Item>
<Select.Item value="unofficial">unofficial</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div>
<Label class="text-sm">{m.packages_arch_select()}</Label>
<Select.Root type="single" bind:value={filterArchitecture}>
<Select.Trigger class="mt-1 w-[180px]">{filterArchitecture}</Select.Trigger>
<Select.Content>
<Select.Item value="">none</Select.Item>
<Select.Item value="aarch64-semios-linux">aarch64-semios-linux</Select.Item>
<Select.Item value="x86_64-semios-linux">x86_64-semios-linux</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
<Input placeholder={m.packages_search_keyword()} bind:value={searchKeyword} class="mt-4 md:mt-0 md:w-1/2" />
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<Checkbox bind:checked={showLatestOnly} />
<span>{m.packages_show_latest_only()}</span>
</div>
</div>
</Card.Content>
<Card.Footer>
<Button variant="default" onclick={search}>
{m.packages_search_action()}
</Button>
</Card.Footer>
</Card.Root>
</div>
<div class="mt-4 overflow-x-auto">
<div class="mt-4 overflow-x-auto"> <Table.Root>
<Table.Root> <Table.Header>
<Table.Header> <Table.Row>
<Table.Row> <Table.Head>{m.packages_name()}</Table.Head>
<Table.Head>{m.packages_name()}</Table.Head> <Table.Head>{m.packages_version()}</Table.Head>
<Table.Head>{m.packages_version()}</Table.Head> <Table.Head>{m.packages_arch()}</Table.Head>
<Table.Head>{m.packages_arch()}</Table.Head> </Table.Row>
</Table.Row> </Table.Header>
</Table.Header> <Table.Body>
<Table.Body> {#each results as item (strPkgIdent(item))}
{#each results as item} <Table.Row>
<Table.Row> <Table.Cell>
<Table.Cell><a href={`/packages/pkginfo?pkgIdent=${strPkgIdent(item)}&mirrorName=${selectedMirror}`}>{item.name}</a></Table.Cell> <a href={pkgInfoUrl(selectedMirror, item)}>
<Table.Cell>{item.version}</Table.Cell> {item.name}
<Table.Cell>{item.arch}</Table.Cell> </a>
</Table.Row> </Table.Cell>
{/each} <Table.Cell>{item.version}</Table.Cell>
</Table.Body> <Table.Cell>{item.arch}</Table.Cell>
</Table.Root> </Table.Row>
</div> {/each}
</div> </Table.Body>
</Table.Root>
</div>
</div>
+6 -3
View File
@@ -41,7 +41,9 @@ export function baseUrlForMirror(mirror: string): string {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function getRepoDb(url: string): Promise<any> { export async function getRepoDb(url: string): Promise<any> {
const sqlPromise = initSqlJs(); const sqlPromise = initSqlJs({
locateFile: () => "/sql-wasm-browser.wasm",
});
const dataPromise = fetch(url).then((res) => res.arrayBuffer()); const dataPromise = fetch(url).then((res) => res.arrayBuffer());
const [SQL, data] = await Promise.all([sqlPromise, dataPromise]); const [SQL, data] = await Promise.all([sqlPromise, dataPromise]);
return new SQL.Database(new Uint8Array(data)); return new SQL.Database(new Uint8Array(data));
@@ -64,14 +66,15 @@ export function searchPackages(db: any, keyword: string): PkgIdent[] {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getPkgInfo(db: any, pkg: PkgIdent): any { export function getPkgInfo(db: any, pkg: PkgIdent): any {
const stmt = db.prepare( const stmt = db.prepare(
`SELECT pkg_manifest, download_size, installed_size FROM package WHERE pkg_name = ? AND pkg_version = ? AND pkg_arch = ?` `SELECT pkg_manifest, download_size, installed_size, checksum FROM package WHERE pkg_name = ? AND pkg_version = ? AND pkg_arch = ?`
); );
stmt.bind([pkg.name, pkg.version, pkg.arch]); stmt.bind([pkg.name, pkg.version, pkg.arch]);
if (stmt.step()) { if (stmt.step()) {
const [sManifest, downloadSize, installedSize] = stmt.get(); const [sManifest, downloadSize, installedSize, checksum] = stmt.get();
const manifest = JSON.parse(sManifest); const manifest = JSON.parse(sManifest);
manifest['download_size'] = downloadSize; manifest['download_size'] = downloadSize;
manifest['installed_size'] = installedSize; manifest['installed_size'] = installedSize;
manifest['checksum'] = checksum
return manifest; return manifest;
} }
return null; return null;
+194 -104
View File
@@ -1,116 +1,206 @@
<script lang="ts"> <script lang="ts">
// Required search params: // Required search params:
// - pkgIdent // - pkgIdent
// - mirrorName // - mirrorName
import { page } from '$app/state'; import { page } from '$app/state';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import * as m from "$lib/paraglide/messages"; import * as m from '$lib/paraglide/messages';
import { Button } from "$lib/components/ui/button/index.js"; import { Button } from '$lib/components/ui/button/index.js';
import * as Card from "$lib/components/ui/card/index.js"; import * as Card from '$lib/components/ui/card/index.js';
import * as Table from "$lib/components/ui/table/index.js"; import * as Table from '$lib/components/ui/table/index.js';
import * as Avatar from '$lib/components/ui/avatar/index.js';
import * as Tabs from '$lib/components/ui/tabs/index.js';
import * as Item from '$lib/components/ui/item/index.js';
import { toast } from 'svelte-sonner';
import { getRepoDb, parsePkgIdent, baseUrlForMirror, getPkgInfo, formatSize, type PkgIdent, strPkgIdent } from '../helper.ts'; import {
getRepoDb,
parsePkgIdent,
baseUrlForMirror,
getPkgInfo,
formatSize,
type PkgIdent,
strPkgIdent
} from '../helper.ts';
import { ArrowDownToLine, ClipboardCopy, Package, Ungroup, UsersRound } from '@lucide/svelte';
import Label from '$lib/components/ui/label/label.svelte';
let baseUrl: string | null = $state(null); let baseUrl: string | null = $state(null);
let pkgInfo = $state(null); let pkgInfo = $state(null);
let pkgIdent: PkgIdent | null = $state(null); let pkgIdent: PkgIdent | null = $state(null);
let mirrorName: string | null = $state(null); let mirrorName: string | null = $state(null);
onMount(async () => { onMount(async () => {
const sPkgIdent = page.url.searchParams.get('pkgIdent'); const sPkgIdent = page.url.searchParams.get('pkgIdent');
pkgIdent = sPkgIdent ? parsePkgIdent(sPkgIdent) : null; pkgIdent = sPkgIdent ? parsePkgIdent(sPkgIdent) : null;
if (!pkgIdent) { if (!pkgIdent) {
error(404); error(404);
} }
mirrorName = page.url.searchParams.get('mirrorName'); mirrorName = page.url.searchParams.get('mirrorName');
baseUrl = mirrorName ? baseUrlForMirror(mirrorName) : null; baseUrl = mirrorName ? baseUrlForMirror(mirrorName) : null;
if (!baseUrl) { if (!baseUrl) {
error(404); error(404);
} }
const db = await getRepoDb(`${baseUrl}/repo.db`); const db = await getRepoDb(`${baseUrl}/repo.db`);
if (!db) { if (!db) {
error(404); error(404);
} }
pkgInfo = getPkgInfo(db, pkgIdent); pkgInfo = getPkgInfo(db, pkgIdent);
if (!pkgInfo) { if (!pkgInfo) {
error(404); error(404);
} }
}) });
</script> </script>
<div class="ml-4 mr-4 flex flex-col md:flex-row gap-4"> <div class="mr-4 ml-4 flex flex-col gap-4 md:flex-row">
<div class="md:w-1/2 flex flex-col gap-4"> <div class="flex flex-col gap-4 md:w-1/2">
<Card.Root class="h-fit"> <Card.Root class="h-fit">
<Card.Header> <Card.Header>
<Card.Title class="flex items-center gap-2"> <Card.Title class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-package-icon lucide-package"><path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z"/><path d="M12 22V12"/><polyline points="3.29 7 12 12 20.71 7"/><path d="m7.5 4.27 9 5.15"/></svg> <Package />
{pkgInfo ? pkgInfo['name'] : ""} {pkgInfo ? pkgInfo['name'] : ''}
</Card.Title> </Card.Title>
<Card.Description>{pkgInfo ? pkgInfo['description'] : null}</Card.Description> <Card.Description>{pkgInfo ? pkgInfo['description'] : null}</Card.Description>
</Card.Header> </Card.Header>
<Card.Content> <Card.Content>
<p>{m.pkginfo_version()}: {pkgInfo ? pkgInfo['version'] : ""}</p> <div class="flex">
<p>{m.pkginfo_arch()}: {pkgInfo ? pkgInfo['arch'] : ""}</p> <Label class="font-bold">{m.pkginfo_version()}:&nbsp;</Label>
<p>{m.pkginfo_installed_size()}: {pkgInfo ? formatSize(pkgInfo['installed_size']) : ""}</p> <span>{pkgInfo ? pkgInfo['version'] : ''}</span>
</Card.Content> </div>
</Card.Root> <div class="flex">
<Card.Root class="h-fit"> <Label class="font-bold">{m.pkginfo_arch()}:&nbsp;</Label>
<Card.Header> <span>{pkgInfo ? pkgInfo['arch'] : ''}</span>
<Card.Title class="flex items-center gap-2"> </div>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-down-to-line-icon lucide-arrow-down-to-line"><path d="M12 17V3"/><path d="m6 11 6 6 6-6"/><path d="M19 21H5"/></svg> <div class="flex">
{m.pkginfo_download_title()} <Label class="font-bold">{m.pkginfo_installed_size()}:&nbsp;</Label>
</Card.Title> <span>{pkgInfo ? formatSize(pkgInfo['installed_size']) : ''}</span>
</Card.Header> </div>
<Card.Content> </Card.Content>
<p>{m.pkginfo_download_filename()}: {(pkgIdent ? strPkgIdent(pkgIdent) : "") + ".pkg"}</p> </Card.Root>
<p>{m.pkginfo_download_size()}: {pkgInfo ? formatSize(pkgInfo['download_size']) : ""}</p> <Card.Root class="h-fit">
</Card.Content> <Card.Header>
<Card.Footer> <Card.Title class="flex items-center gap-2">
<Button onclick={() => { <ArrowDownToLine />
window.location.href = `${baseUrl}/${(pkgIdent ? strPkgIdent(pkgIdent) : '') + '.pkg'}`; {m.pkginfo_install_title()}
}}>{m.pkginfo_download_action()}</Button> </Card.Title>
</Card.Footer> </Card.Header>
</Card.Root> <Card.Content>
</div> <Tabs.Root value="file">
<div class="md:w-1/2"> <Tabs.List>
<Card.Root> <Tabs.Trigger value="file">{m.pkginfo_install_download()}</Tabs.Trigger>
<Card.Header> <Tabs.Trigger value="packie">Packie</Tabs.Trigger>
<Card.Title class="flex items-center gap-2"> </Tabs.List>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ungroup-icon lucide-ungroup"><rect x="11" y="14" width="10" height="7" rx="2"/><rect x="3" y="3" width="10" height="7" rx="2"/></svg> <Tabs.Content value="file" class="mt-2">
{m.pkginfo_dependency_title()} <div class="flex min-w-0 items-center">
</Card.Title> <Label class="shrink-0 font-bold">{m.pkginfo_download_filename()}:&nbsp;</Label>
<Card.Description>{m.pkginfo_dependency_description()}</Card.Description> <span class="truncate font-mono text-sm">
</Card.Header> {(pkgIdent ? strPkgIdent(pkgIdent) : '') + '.pkg'}
<Card.Content> </span>
<Table.Root> </div>
<Table.Header> <div class="flex">
<Table.Row> <Label class="font-bold">{m.pkginfo_download_size()}:&nbsp;</Label>
<Table.Head>{m.pkginfo_dependency_pkgspec()}</Table.Head> <span>{pkgInfo ? formatSize(pkgInfo['download_size']) : ''}</span>
<Table.Head>{m.pkginfo_dependency_type()}</Table.Head> </div>
</Table.Row> <div class="flex">
</Table.Header> <Label class="shrink-0 font-bold">{m.pkginfo_download_checksum()}:&nbsp;</Label>
<Table.Body> <code class="block overflow-x-auto bg-gray-100 whitespace-nowrap dark:bg-gray-800">
{#each pkgInfo ? pkgInfo['dependencies'] : [] as item} {pkgInfo ? pkgInfo['checksum'] : ''}
<Table.Row> </code>
<Table.Cell>{item}</Table.Cell> </div>
<Table.Cell>{m.pkginfo_dependency_type_required()}</Table.Cell> <Button
</Table.Row> class="mt-3"
{/each} onclick={() => {
{#each pkgInfo ? pkgInfo['recommendations'] : [] as item} window.location.href = `${baseUrl}/${(pkgIdent ? strPkgIdent(pkgIdent) : '') + '.pkg'}`;
<Table.Row> }}>{m.pkginfo_download_action()}</Button
<Table.Cell>{item}</Table.Cell> >
<Table.Cell>{m.pkginfo_dependency_type_recommended()}</Table.Cell> </Tabs.Content>
</Table.Row> <Tabs.Content value="packie" class="mt-2">
{/each} <p>{m.pkginfo_install_packie_prompt_terminal()}</p>
</Table.Body> <Item.Root variant="muted" class="flex items-center gap-2 mt-2">
</Table.Root> <Item.Content class="min-w-0 flex-1">
</Card.Content> <div class="flex items-center overflow-x-auto whitespace-nowrap">
</Card.Root> <Label class="shrink-0">$&nbsp;</Label>
</div> <code class="font-mono text-sm">
packie install '{pkgIdent ? strPkgIdent(pkgIdent) : ''}'
</code>
</div>
</Item.Content>
<Item.Actions class="shrink-0">
<Button
variant="default"
size="icon"
onclick={() => {
const command = `packie install '${pkgIdent ? strPkgIdent(pkgIdent) : ''}'`;
navigator.clipboard.writeText(command);
toast(m.pkginfo_install_packie_pasted(), { description: command });
}}
>
<ClipboardCopy />
</Button>
</Item.Actions>
</Item.Root>
</Tabs.Content>
</Tabs.Root>
</Card.Content>
</Card.Root>
</div>
<div class="flex flex-col gap-4 md:w-1/2">
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
<Ungroup />
{m.pkginfo_dependency_title()}
</Card.Title>
<Card.Description>{m.pkginfo_dependency_description()}</Card.Description>
</Card.Header>
<Card.Content>
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>{m.pkginfo_dependency_pkgspec()}</Table.Head>
<Table.Head>{m.pkginfo_dependency_type()}</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each pkgInfo ? pkgInfo['dependencies'] : [] as item (item)}
<Table.Row>
<Table.Cell>{item}</Table.Cell>
<Table.Cell>{m.pkginfo_dependency_type_required()}</Table.Cell>
</Table.Row>
{/each}
{#each pkgInfo ? pkgInfo['recommendations'] : [] as item (item)}
<Table.Row>
<Table.Cell>{item}</Table.Cell>
<Table.Cell>{m.pkginfo_dependency_type_recommended()}</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
<UsersRound />
{m.pkginfo_maintainers()}
</Card.Title>
</Card.Header>
<Card.Content>
{#each pkgInfo ? pkgInfo['maintainers'] : [] as item (item)}
<div>
<Avatar.Root>
<Avatar.Image />
<Avatar.Fallback>User</Avatar.Fallback>
</Avatar.Root>
</div>
{/each}
</Card.Content>
</Card.Root>
</div>
</div> </div>
+10
View File
@@ -0,0 +1,10 @@
<script>
import Separator from "$lib/components/ui/separator/separator.svelte";
</script>
<div class="ml-6 mr-6 mt-6">
<h1 class="text-3xl font-bold">Privacy Policy</h1>
<p class="text-muted-foreground text-sm mt-1">Updated: 2026-08-18</p>
<Separator class="mt-2 mb-2" />
This privacy policy describes how we use your personal data collected when you access our website.
</div>