Initial DruksCode V2: fresh fork of web-to-app (f3b488e) with DruksCode identity
Some checks failed
Android CI Build / check (push) Has been cancelled
Android CI Build / package-apks (push) Has been cancelled
Docs Deploy / build (push) Has been cancelled
Module Market Check / Validate modules/ (push) Has been cancelled
Module Market Publish / Generate submissions.json (push) Has been cancelled
Docs Deploy / deploy (push) Has been cancelled
Some checks failed
Android CI Build / check (push) Has been cancelled
Android CI Build / package-apks (push) Has been cancelled
Docs Deploy / build (push) Has been cancelled
Module Market Check / Validate modules/ (push) Has been cancelled
Module Market Publish / Generate submissions.json (push) Has been cancelled
Docs Deploy / deploy (push) Has been cancelled
- Package rename com.webtoapp -> br.com.drukstech.codeapp - Product rename WebToApp/web-to-app -> DruksCode/drukscode - Design system Wta* -> Dkc* - applicationId br.com.drukstech.codeapp (.play for gplay flavor) - rootProject.name DruksCode - Fresh git history (upstream kept as remote for sync)
This commit is contained in:
commit
e9899d2e6d
449
.github/CONTRIBUTING.md
vendored
Normal file
449
.github/CONTRIBUTING.md
vendored
Normal file
@ -0,0 +1,449 @@
|
||||
# Contributing to DruksCode
|
||||
|
||||
Thanks for taking the time to help. DruksCode moves fastest when contributions
|
||||
stay **small and well-scoped** — pick one of the lanes below and ignore the
|
||||
rest.
|
||||
|
||||
> **English** · [简体中文](#贡献-webtoapp中文)
|
||||
|
||||
This guide targets **DruksCode 2.6.0** (`versionCode 62`).
|
||||
|
||||
---
|
||||
|
||||
## Lanes
|
||||
|
||||
| You want to… | Go to | Effort |
|
||||
| --- | --- | --- |
|
||||
| Publish a JS/CSS module to the in-app **Module Market** | [`modules/README.md`](../modules/README.md) | hours |
|
||||
| File a bug, request a feature, or ask a question | [GitHub Issues](https://github.com/shiaho777/web-to-app/issues) | minutes |
|
||||
| Fix a bug or build a feature in the Android client | This guide ↓ | days |
|
||||
|
||||
If you're not sure which lane fits, open an issue or a discussion first. There
|
||||
is no need to write code before there's agreement on the shape of the change.
|
||||
|
||||
---
|
||||
|
||||
## Module Market submissions
|
||||
|
||||
The fastest way to ship something useful to every DruksCode user is to publish
|
||||
a module. The canonical Module Market guide lives in
|
||||
[`modules/README.md`](../modules/README.md). Use that file for the full
|
||||
schema, field rules, reviewer checklist, and CI validation details. The short
|
||||
version here is only meant to help you pick the right contribution lane:
|
||||
|
||||
1. Fork the repo.
|
||||
2. Add `modules/<your-module>/module.json` and `main.js` (plus `style.css` if
|
||||
you need CSS).
|
||||
3. Add an entry to `modules/registry.json`.
|
||||
4. Open a PR.
|
||||
|
||||
The market has **no backend**. Clients read `registry.json` and
|
||||
`submissions.json`, and only merged modules show up in the catalog. Module
|
||||
changes are validated in CI by `.github/scripts/ci/validate_modules.py`:
|
||||
|
||||
```bash
|
||||
python3 .github/scripts/ci/validate_modules.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code contributions
|
||||
|
||||
> The canonical working guide for AI coding agents and deep code
|
||||
> contributors is [`AGENTS.md`](../AGENTS.md) — it documents how the editor,
|
||||
> export pipeline, shell template, and runtime connect, the dual preview/export
|
||||
> paths, config-field drift rules, packaging constraints, and common change
|
||||
> recipes. Read it before any non-trivial change to the build, shell sync,
|
||||
> export packaging, or config fields.
|
||||
|
||||
### Before you write code
|
||||
|
||||
- Search [issues](https://github.com/shiaho777/web-to-app/issues) for prior
|
||||
discussion of the same idea.
|
||||
- For non-trivial changes, open an issue first describing the problem and the
|
||||
approach you have in mind. This is much cheaper than rewriting after review.
|
||||
- **Avoid adding new dependencies.** The lists in `app/build.gradle.kts` and
|
||||
`shell/build.gradle.kts` are intentionally restrained — the project signs and
|
||||
packages APKs in-process, and the shell template pins `targetSdk = 28` on
|
||||
purpose because generated apps rely on fork+exec native runtimes. New
|
||||
dependencies need a strong justification, and host-only dependencies must
|
||||
never leak into the shell template.
|
||||
|
||||
### Local setup
|
||||
|
||||
You'll need:
|
||||
|
||||
- Android Studio Hedgehog or newer
|
||||
- JDK 17
|
||||
- The Gradle wrapper pins Gradle 9.4.1 — no system Gradle install required
|
||||
|
||||
```bash
|
||||
git clone https://github.com/shiaho777/web-to-app.git
|
||||
cd drukscode
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
|
||||
The repo has three Gradle modules: **`app`** (the full builder and host),
|
||||
**`shell`** (the runtime template embedded in generated APKs), and
|
||||
**`clone-host`** (the host code used by app cloning). Shared runtime code is
|
||||
authored **only** under `app/` and synchronized into `shell/` at build time
|
||||
(`shell/build/generated/` — never edit `shell/src` by hand; it is
|
||||
regenerated and partly git-ignored).
|
||||
|
||||
Run the checks before submitting:
|
||||
|
||||
```bash
|
||||
./gradlew :app:compileStandardDebugKotlin -x syncCloneHostDex --no-configuration-cache
|
||||
./gradlew :app:testStandardDebugUnitTest --no-configuration-cache -PskipShellTemplateSync=true
|
||||
./gradlew :app:checkConfigFieldDrift --no-configuration-cache
|
||||
```
|
||||
|
||||
If you touched shell-synced runtime code or export packaging, also rebuild the
|
||||
template you touched:
|
||||
|
||||
```bash
|
||||
./gradlew :shell:assembleRelease :app:syncShellTemplateApk --no-configuration-cache
|
||||
```
|
||||
|
||||
> Native code (`node_launcher`, `go_exec_loader`, the APK optimizer) builds
|
||||
> via CMake per-ABI and needs the Android NDK + CMake installed through the
|
||||
> SDK Manager. CI installs `cmake;3.22.1` and `ndk;28.2.13676358`.
|
||||
|
||||
### Where things live
|
||||
|
||||
| Area | Path |
|
||||
| --- | --- |
|
||||
| App types & central config (`AppType`, `WebApp`) | `app/src/main/java/br/com/drukstech/codeapp/data/model/` |
|
||||
| On-device APK builder / signer | `app/src/main/java/br/com/drukstech/codeapp/core/apkbuilder/` |
|
||||
| Server runtimes (Node / PHP / Python / Go / WordPress) | `app/src/main/java/br/com/drukstech/codeapp/core/{nodejs,php,python,golang,wordpress}/` |
|
||||
| WebView engine, native bridge, fingerprint disguise | `app/src/main/java/br/com/drukstech/codeapp/core/{webview,engine,appearance}/` |
|
||||
| Shell config + generated-APK entry points | `app/src/main/java/br/com/drukstech/codeapp/core/shell/` |
|
||||
| Preview players (`ui/gallery/`, `ui/media/`) vs packaged players (`ui/shell/`) | `app/src/main/java/br/com/drukstech/codeapp/ui/` |
|
||||
| Extension modules, Module Market, Agent | `app/src/main/java/br/com/drukstech/codeapp/core/{extension,market,agent}/` |
|
||||
| Compose UI screens & design system | `app/src/main/java/br/com/drukstech/codeapp/ui/` |
|
||||
| DI graph (source of truth) | `app/src/main/java/br/com/drukstech/codeapp/di/AppModule.kt` |
|
||||
|
||||
### Preview ≠ export: the rule behind most rejections
|
||||
|
||||
The host app (preview) and generated APKs (export) run **different code**:
|
||||
|
||||
| | Preview (`:app`) | Exported APK |
|
||||
| --- | --- | --- |
|
||||
| UI | `ui/gallery/`, `ui/media/`, `WebViewManager` live | `ui/shell/*` synced copy |
|
||||
| Config | Editor / in-memory models | JSON assets via `ShellModeManager` |
|
||||
|
||||
A feature is done only when it works on **both** paths. Concretely, an editor
|
||||
setting that must affect generated APKs has to flow through the full chain —
|
||||
model → `ApkConfig` JSON → shell config → shell-synced runtime — and a
|
||||
player-screen behavior has to be implemented in **both** the preview and the
|
||||
shell composable. Three gates enforce this; run them, don't argue with them:
|
||||
|
||||
- `checkConfigFieldDrift` — model/JSON/shell field names must match
|
||||
(Gson silently drops mismatches).
|
||||
- `WebViewConfigBooleanCoverageTest` — every new `WebViewConfig` Boolean must
|
||||
be listed in `flipAllBooleans()` and survive the export round-trip.
|
||||
- `ShellUiParityTest` — every model field read by host player UI must also be
|
||||
read by shell player UI (or carry an explicit `allow` reason). Add your new
|
||||
per-type player pair to it.
|
||||
|
||||
### Coding conventions
|
||||
|
||||
DruksCode leans on the Kotlin and Jetpack Compose patterns already in the
|
||||
codebase. A few rules worth calling out:
|
||||
|
||||
- **Comments explain *why*, not *what*.** Non-obvious decisions, cross-file
|
||||
contracts, and issue references (`#781`) get a short comment; clear names
|
||||
and small functions carry the rest. Don't narrate readable code.
|
||||
- **Build new UI on the Dkc design system.** Everything renders through
|
||||
`br.com.drukstech.codeapp.ui.design`. The older `Premium*` / `Enhanced*` / `Settings*`
|
||||
components are retained as permanent alias layers over the Dkc internals —
|
||||
don't add new ones, and you don't need to rip them out. A build-time audit
|
||||
(`.github/scripts/audit_ui_design_system.py`, wired into `build.gradle.kts`) tracks
|
||||
legacy UI debt against `.github/scripts/ui_design_allowlist.txt`.
|
||||
- **Reuse the design tokens** in `ui/design/DkcTokens.kt` for spacing, radius,
|
||||
alpha, and elevation. Don't hard-code numbers. Editor config cards share one
|
||||
layout grammar (see `AGENTS.md` recipe 12) — copy the neighbouring cards,
|
||||
don't invent your own, and verify on the emulator, not just by compiling.
|
||||
- **Strings must cover all 10 supported languages.** UI copy lives in
|
||||
`core/i18n/Strings.kt` (facade `object Strings` + in-file `StringsA`…`StringsE`,
|
||||
split only for the JVM constant pool). Supported: Chinese, English, Arabic,
|
||||
Portuguese, Spanish, French, German, Russian, Japanese, Korean.
|
||||
Every new or changed user-visible `when (Strings.lang)` block **must** have
|
||||
real translations for **all 10** branches with no `else ->` — do not leave
|
||||
pt/es/fr/de/ru/ja/ko as English placeholders. Brand names and pure format
|
||||
tokens may match English. If you truly cannot translate one language, use
|
||||
English there and flag it in the PR.
|
||||
- **Never load user-visible text from `R.string`.** Resource files only cover
|
||||
zh/en/ar, so lookups silently fall back to Chinese for the other 7 locales.
|
||||
`R.string` is reserved for non-localised resources.
|
||||
- **No new top-level singletons** unless you discuss it first. The DI graph in
|
||||
`di/AppModule.kt` is the source of truth.
|
||||
- **The Native Bridge is capability-gated.** Any method exposed to web content
|
||||
via `@JavascriptInterface` must be guarded by the per-capability allow-list
|
||||
(`NativeBridgeCapabilities`). Never expose a native capability to arbitrary
|
||||
pages without a gate. PRs that bypass this will be rejected.
|
||||
- **Preview and packaged runtime are two paths.** See *Preview ≠ export*
|
||||
above. Shared runtime sources are authored only under `app/` and synced into
|
||||
`shell/` at build time — do not hand-edit anything under `shell/src`.
|
||||
- **Avoid catching `Exception` to silence errors.** If recovery is impossible,
|
||||
log via `AppLogger` and re-throw or return a failed `Result`.
|
||||
|
||||
### Security
|
||||
|
||||
Anything touching the WebView, file IO, APK signing, or the native bridge has
|
||||
downstream impact on **every generated app**, so it gets extra scrutiny.
|
||||
Specifically, PRs will be declined if they:
|
||||
|
||||
- store or expose credentials/secrets to web content,
|
||||
- widen the native surface exposed to arbitrary pages without a capability
|
||||
gate,
|
||||
- weaken APK signing, isolation, or the fingerprint-disguise defaults,
|
||||
- bundle unrelated changes inside a large diff.
|
||||
|
||||
### Commit messages
|
||||
|
||||
We don't enforce Conventional Commits, but a clear subject line and a wrapped
|
||||
body that explains *why* the change exists make reviews much faster. Example:
|
||||
|
||||
```
|
||||
ModuleMarket: cache registry.json for an hour
|
||||
|
||||
Repeatedly hitting raw.githubusercontent.com on every screen open is wasteful
|
||||
and triggers GitHub's anonymous rate limit on slow connections. Cache the
|
||||
parsed registry under cache/module_market/ and treat anything fresher than
|
||||
an hour as authoritative; the refresh button always bypasses the cache.
|
||||
```
|
||||
|
||||
### Pull requests
|
||||
|
||||
- Branch from `main`. Keep PRs focused — one logical change per PR.
|
||||
- **Write Issues and PRs in English** — titles, bodies, and review threads.
|
||||
- Describe the user-visible effect in the PR body, not just the code change.
|
||||
- The standard loop is Issue → branch → PR (`Fixes #N`) → green CI → merge.
|
||||
Issues close on merge, never on PR open or red CI.
|
||||
- If your PR touches the build system, native code, or APK packaging, attach
|
||||
the output of the template rebuild above (or note the failure if it fails
|
||||
on your machine).
|
||||
- CI runs on every PR. A green CI is required before merge.
|
||||
- Never commit secrets, keystores, `local.properties`, or IDE/cache junk.
|
||||
|
||||
### Reviewer expectations
|
||||
|
||||
Maintainer reviews look for:
|
||||
|
||||
- **Correctness.** Does the change do what the PR description says? Was it
|
||||
verified by execution (tests, build output), not just by reading?
|
||||
- **Safety.** See *Security* above.
|
||||
- **Scope discipline.** Drive-by refactors expand the review surface. Land
|
||||
them in a separate PR.
|
||||
- **Style fit.** See *Coding conventions* above.
|
||||
- **Both paths.** Preview-only features and export-only wiring are the two
|
||||
classic failure modes — see *Preview ≠ export* above.
|
||||
|
||||
---
|
||||
|
||||
## Code of conduct
|
||||
|
||||
Feedback is required to be precise. "This code is garbage" is acceptable only
|
||||
when accompanied by which code, why, and what would be less so. The maintainer
|
||||
(**shiaho**) has on file a standing waiver covering criticism of himself, his
|
||||
judgment, and his life choices. Praise unaccompanied by a working patch is
|
||||
logged and otherwise disregarded.
|
||||
|
||||
---
|
||||
|
||||
<a id="贡献-webtoapp中文"></a>
|
||||
## 贡献 DruksCode(中文)
|
||||
|
||||
非常感谢你愿意花时间。DruksCode 的迭代速度取决于"小而聚焦"的贡献——下面三条路
|
||||
里挑一条走,其他的先忽略。
|
||||
|
||||
本指南对应 **DruksCode 2.6.0**(`versionCode 62`)。
|
||||
|
||||
### 你想做什么?
|
||||
|
||||
| 你想…… | 路径 | 投入 |
|
||||
| --- | --- | --- |
|
||||
| 给应用内的 **模块市场** 提交一个 JS/CSS 模块 | [`modules/README.md`](../modules/README.md) | 几小时 |
|
||||
| 报 Bug、提 Feature、问问题 | [GitHub Issues](https://github.com/shiaho777/web-to-app/issues) | 几分钟 |
|
||||
| 修 Bug 或在 Android 客户端里做新功能 | 见下方代码贡献小节 | 几天 |
|
||||
|
||||
不确定走哪条,先开 issue 或 discussion——动手前对齐方向,比写完再返工便宜
|
||||
得多。
|
||||
|
||||
### 模块市场贡献
|
||||
|
||||
让你的工作触达每一个用户最快的方式就是提一个模块。Schema、审核 Checklist 和
|
||||
CI 校验细节的主文档是 [`modules/README.md`](../modules/README.md)。这里仅保
|
||||
留一个极简入口,方便你先判断自己是不是走这条贡献路线:
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 新建 `modules/<你的模块>/module.json` 和 `main.js`(需要 CSS 时再加
|
||||
`style.css`)
|
||||
3. 在 `modules/registry.json` 里加一行索引
|
||||
4. 提 PR
|
||||
|
||||
市场**没有后端**。客户端读取 `registry.json` 和 `submissions.json`,只有已合
|
||||
并的模块才会在市场出现。模块改动会经过 CI 的
|
||||
`.github/scripts/ci/validate_modules.py` 校验,提 PR 前建议先本地跑:
|
||||
|
||||
```bash
|
||||
python3 .github/scripts/ci/validate_modules.py
|
||||
```
|
||||
|
||||
### 代码贡献
|
||||
|
||||
> 面向 AI 编码助手与深度代码贡献者的权威工作指南是
|
||||
> [`AGENTS.md`](../AGENTS.md) —— 它讲清了编辑器、导出流水线、shell 模板与运行时
|
||||
> 之间如何连接,以及双路径预览 / 导出、配置字段漂移规则、打包约束和常见改动
|
||||
> 配方。对构建、shell 同步、导出打包或配置字段的任何非平凡改动,动手前请先读。
|
||||
|
||||
**动手前**
|
||||
|
||||
- 在 [issues](https://github.com/shiaho777/web-to-app/issues) 里先搜一下
|
||||
类似讨论
|
||||
- 较大的改动请先开 issue 说明要解决的问题和方案
|
||||
- **谨慎引入新依赖**。`app/build.gradle.kts` 与 `shell/build.gradle.kts` 的依
|
||||
赖列表刻意保持精简——本项目全程在设备内签名打包 APK,shell 模板也特意把
|
||||
`targetSdk` 锁在 28,因为生成应用依赖 `fork`、`exec` 原生运行时。新依赖需
|
||||
要充分理由,且宿主专用依赖绝不能漏进 shell 模板。
|
||||
|
||||
**本地环境**
|
||||
|
||||
- Android Studio Hedgehog 或更新版本
|
||||
- JDK 17
|
||||
- Gradle wrapper 已锁定 Gradle 9.4.1,无需系统安装 Gradle
|
||||
|
||||
```bash
|
||||
git clone https://github.com/shiaho777/web-to-app.git
|
||||
cd drukscode
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
|
||||
仓库有三个 Gradle 模块:**`app`**(完整构建器和宿主)、**`shell`**(嵌入生
|
||||
成 APK 的运行时模板)和 **`clone-host`**(应用克隆使用的宿主代码)。共享运
|
||||
行时代码以 `app/` 为唯一事实来源,构建时同步到 `shell/`(`shell/build/` 下,
|
||||
不要手改 `shell/src`——它是再生成的产物,部分被 git 忽略)。
|
||||
|
||||
提交前请跑通:
|
||||
|
||||
```bash
|
||||
./gradlew :app:compileStandardDebugKotlin -x syncCloneHostDex --no-configuration-cache
|
||||
./gradlew :app:testStandardDebugUnitTest --no-configuration-cache -PskipShellTemplateSync=true
|
||||
./gradlew :app:checkConfigFieldDrift --no-configuration-cache
|
||||
```
|
||||
|
||||
动到 shell 同步的运行时代码或导出打包,还要重建你碰过的模板:
|
||||
|
||||
```bash
|
||||
./gradlew :shell:assembleRelease :app:syncShellTemplateApk --no-configuration-cache
|
||||
```
|
||||
|
||||
> 原生代码(`node_launcher`、`go_exec_loader`、APK 优化器)按 ABI 经 CMake
|
||||
> 编译,需要通过 SDK Manager 安装 Android NDK + CMake。CI 安装的是
|
||||
> `cmake;3.22.1` 与 `ndk;28.2.13676358`。
|
||||
|
||||
**代码大致位置**
|
||||
|
||||
| 区域 | 路径 |
|
||||
| --- | --- |
|
||||
| 应用类型与核心配置(`AppType`、`WebApp`) | `app/src/main/java/br/com/drukstech/codeapp/data/model/` |
|
||||
| 设备端 APK 打包 / 签名 | `app/src/main/java/br/com/drukstech/codeapp/core/apkbuilder/` |
|
||||
| 服务端运行时(Node / PHP / Python / Go / WordPress) | `app/src/main/java/br/com/drukstech/codeapp/core/{nodejs,php,python,golang,wordpress}/` |
|
||||
| WebView 引擎、原生桥、指纹伪装 | `app/src/main/java/br/com/drukstech/codeapp/core/{webview,engine,appearance}/` |
|
||||
| Shell 配置与生成 APK 入口 | `app/src/main/java/br/com/drukstech/codeapp/core/shell/` |
|
||||
| 预览播放器(`ui/gallery/`、`ui/media/`)vs 打包播放器(`ui/shell/`) | `app/src/main/java/br/com/drukstech/codeapp/ui/` |
|
||||
| 扩展模块、模块市场、Agent | `app/src/main/java/br/com/drukstech/codeapp/core/{extension,market,agent}/` |
|
||||
| Compose UI 与设计系统 | `app/src/main/java/br/com/drukstech/codeapp/ui/` |
|
||||
| DI 依赖图(单一事实来源) | `app/src/main/java/br/com/drukstech/codeapp/di/AppModule.kt` |
|
||||
|
||||
**预览 ≠ 导出:大多数打回的原因**
|
||||
|
||||
宿主应用(预览)和生成的 APK(导出)跑的是**两套代码**:
|
||||
|
||||
| | 预览(`:app`) | 导出的 APK |
|
||||
| --- | --- | --- |
|
||||
| 界面 | `ui/gallery/`、`ui/media/`、实时 `WebViewManager` | `ui/shell/*` 的同步副本 |
|
||||
| 配置 | 编辑器 / 内存模型 | 经 `ShellModeManager` 读取的 JSON 资源 |
|
||||
|
||||
一个功能只有两边都通才算做完。编辑器开关要影响生成 APK,必须走完整条
|
||||
链——模型 → `ApkConfig` JSON → shell 配置 → shell 同步的运行时代码;而播放
|
||||
器界面的行为要在预览和壳两套 composable 里各实现一遍。三道门禁负责兜底,
|
||||
跑过它们,别跟它们争:
|
||||
|
||||
- `checkConfigFieldDrift`——模型 / JSON / shell 字段名必须一致(Gson 会静默
|
||||
丢掉对不上的字段)。
|
||||
- `WebViewConfigBooleanCoverageTest`——`WebViewConfig` 每新增一个 Boolean 都
|
||||
必须进 `flipAllBooleans()` 并走完导出往返。
|
||||
- `ShellUiParityTest`——宿主播放器读的每个模型字段,壳播放器也必须读(或写
|
||||
明 `allow` 理由)。新增每类播放器时照样子扩展。
|
||||
|
||||
**代码风格**
|
||||
|
||||
- **注释解释 *为什么*,不复述 *干了什么*。** 反直觉的决策、跨文件约定、
|
||||
issue 引用(`#781`)值得一句短注释;命名清晰的小函数不需要解说。不要给
|
||||
易读的代码写旁白。
|
||||
- **新 UI 一律构建在 Dkc 设计系统之上**——所有界面通过 `br.com.drukstech.codeapp.ui.design`
|
||||
渲染。旧的 `Premium*` / `Enhanced*` / `Settings*` 组件作为 Dkc 内部实现的
|
||||
永久别名层保留——不要再新增,也无需强行替换。构建期有一个审计脚本
|
||||
(`.github/scripts/audit_ui_design_system.py`,接进 `build.gradle.kts`)按
|
||||
`.github/scripts/ui_design_allowlist.txt` 跟踪历史 UI 债务。
|
||||
- 复用 `ui/design/DkcTokens.kt` 里的设计 token(间距、圆角、透明度、高度),
|
||||
别硬编码数字。编辑器配置卡片共用一套排版语法(见 `AGENTS.md` recipe 12)——
|
||||
照抄相邻卡片,不要自创,跑模拟器验效果,不要只编译。
|
||||
- **字符串必须覆盖全部 10 种已支持语言**:文案在 `core/i18n/Strings.kt`
|
||||
(facade `object Strings` + 同文件 `StringsA`…`StringsE`,拆分只为常量池)。
|
||||
已支持:中 / 英 / 阿 / 葡 / 西 / 法 / 德 / 俄 / 日 / 韩。
|
||||
新增或修改面向用户的 `when (Strings.lang)` **必须**为 10 个分支写真实翻译,
|
||||
且不许写 `else ->`;禁止把 pt/es/fr/de/ru/ja/ko 继续当英文占位。品牌名、
|
||||
纯格式符可与英文相同。某语实在不会翻可暂填英文并在 PR 标明。
|
||||
- **面向用户的文案绝不用 `R.string`。** 资源文件只有 zh/en/ar 三套,其他 7
|
||||
语会静默回落到中文。`R.string` 只保留给无需本地化的资源。
|
||||
- 引入新的全局单例前请先讨论;`di/AppModule.kt` 是单一事实来源
|
||||
- **原生桥是按能力门禁的**:任何通过 `@JavascriptInterface` 暴露给网页的方法,
|
||||
都必须经过逐能力白名单(`NativeBridgeCapabilities`)。绝不要在没有门禁的
|
||||
情况下把原生能力暴露给任意页面。绕过门禁的 PR 会被拒
|
||||
- **预览和打包运行时是两条路径**:见上文《预览 ≠ 导出》。共享运行时代码只改
|
||||
`app/`,构建时同步进 `shell/`;不要手改 `shell/src` 下的任何东西
|
||||
- 不要 catch 然后吞掉异常;用 `AppLogger` 记录后重新抛出或返回失败的 `Result`
|
||||
|
||||
**安全**
|
||||
|
||||
动到 WebView、文件 IO、APK 签名或原生桥的改动会影响**每一个生成出来的
|
||||
应用**,因此审核更严。出现以下情况的 PR 会被拒:
|
||||
|
||||
- 把凭据 / 密钥存储或暴露给网页内容
|
||||
- 在没有能力门禁的情况下扩大暴露给任意页面的原生面
|
||||
- 削弱 APK 签名、隔离或指纹伪装的默认强度
|
||||
- 在一个大 diff 里夹带不相关的改动
|
||||
|
||||
**Commit 消息**
|
||||
|
||||
不强制 Conventional Commits,但请把"为什么"写进正文。示例见英文段。
|
||||
|
||||
**Pull Request**
|
||||
|
||||
- 从 `main` 分出分支,每个 PR 只解决一件事
|
||||
- **Issue 与 PR 请用英文写**——标题、正文、评审讨论
|
||||
- PR 描述写"用户看得到的效果",不只是代码 diff
|
||||
- 标准流程是 Issue → 分支 → PR(`Fixes #N`)→ CI 变绿 → 合并。
|
||||
Issue 只在合并时关闭,开 PR 时不关、CI 红时不关
|
||||
- 改动涉及构建系统、原生代码或 APK 打包时,附上上面模板重建命令的结果
|
||||
- CI 必须绿色才会合并
|
||||
- 不要提交密钥、keystore、`local.properties` 或 IDE / 缓存垃圾
|
||||
|
||||
**Review 标准**
|
||||
|
||||
- **正确性**——是否真的做了 PR 描述里写的事;是否经过执行验证(测试、
|
||||
构建产物),而不只是"读起来对"
|
||||
- **安全**——见上面《安全》小节
|
||||
- **聚焦**——顺手做的重构请单独发 PR
|
||||
- **风格**——参考上面那条
|
||||
- **两条路径**——只在预览生效的功能和只在导出接线的配置是两类经典翻车——见
|
||||
上文《预览 ≠ 导出》
|
||||
|
||||
### 行为准则
|
||||
|
||||
反馈须精准。"这代码是坨屎"仅在同时说明"哪段代码、为何如此、怎样才不至于"
|
||||
时方予受理。维护者(**shiaho**)已就针对其本人、其判断力及其人生选择的批评
|
||||
出具长期豁免一份并存档。未附可用补丁的赞美,予以记录,余不受理。
|
||||
BIN
.github/assets/social-preview.png
vendored
Normal file
BIN
.github/assets/social-preview.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 576 KiB |
BIN
.github/assets/sponsors/swiftproxy-banner-en.png
vendored
Normal file
BIN
.github/assets/sponsors/swiftproxy-banner-en.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 419 KiB |
BIN
.github/assets/sponsors/swiftproxy-banner-zh.png
vendored
Normal file
BIN
.github/assets/sponsors/swiftproxy-banner-zh.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
BIN
.github/assets/sponsors/swiftproxy-logo-full.png
vendored
Normal file
BIN
.github/assets/sponsors/swiftproxy-logo-full.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
365
.github/docs/README_CN.md
vendored
Normal file
365
.github/docs/README_CN.md
vendored
Normal file
@ -0,0 +1,365 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" width="96" alt="DruksCode 图标" />
|
||||
|
||||
# DruksCode
|
||||
|
||||
### 在手机上把 Web 项目打包成可安装的 Android APK。
|
||||
|
||||
**一个运行在设备端的 APK 工作台,远不只是套个 WebView —— 它能在手机上 fork+exec 完整的服务端运行时,内置硬核反审查网络栈,能在设备内签名上架 Google Play 的 AAB,还能跑 MV3 浏览器扩展,全程不需要电脑或远程构建服务器。**
|
||||
|
||||
[English](../../README.md) · **简体中文**
|
||||
|
||||
[](https://github.com/shiaho777/web-to-app/stargazers)
|
||||
[](https://github.com/shiaho777/web-to-app/network/members)
|
||||
[](../../LICENSE)
|
||||
[](#)
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.swiftproxy.net/?ref=shiaho">
|
||||
<img src="../assets/sponsors/swiftproxy-logo-full.png" alt="Swiftproxy" width="220" />
|
||||
</a><br/>
|
||||
<a href="https://www.swiftproxy.net/?ref=shiaho"><b>由 Swiftproxy 赞助</b></a> —— 高质量纯净住宅IP提供商
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#webtoapp-有什么不同">有什么不同</a> ·
|
||||
<a href="#能力速览">能力速览</a> ·
|
||||
<a href="#能打包什么">能打包什么</a> ·
|
||||
<a href="#完整能力地图">完整能力地图</a> ·
|
||||
<a href="#模块市场">模块市场</a> ·
|
||||
<a href="#文档站点">文档站点</a> ·
|
||||
<a href="#架构说明">架构说明</a> ·
|
||||
<a href="#从源码构建">构建</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<img src="../assets/social-preview.png" width="90%" alt="DruksCode:我的应用首页、应用列表与主工具箱(Agent、扩展模块、端口管理器)、单个应用的 APK 操作(编辑、创建快捷方式、构建/分享/导出 APK),以及关于页面——全部运行在 Android 手机上" />
|
||||
</div>
|
||||
|
||||
### 演示视频
|
||||
|
||||
<details>
|
||||
<summary><b>观看:在设备上构建并打包一个应用的快速演示</b></summary>
|
||||
|
||||
<video src="https://github.com/user-attachments/assets/4e1a476d-2687-4d97-90a9-d077a15ebe31" controls muted loop playsinline>
|
||||
您的浏览器不支持 video 标签。
|
||||
</video>
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## DruksCode 有什么不同
|
||||
|
||||
绝大多数「网站转 App」工具到「套一个 WebView」就结束了。DruksCode 更像一个放在手机里的 APK 工作台,而真正难的地方,正是它和那些工具的分界线:
|
||||
|
||||
- **在设备上跑真实的服务端运行时。** Node.js、PHP、Python、Go、WordPress 以原生二进制的形式直接从 app 存储 fork+exec —— 像 Termux 那样,但被打进一个可安装的 APK。URL 套壳工具根本做不到。
|
||||
- **内置硬核、反审查的网络栈。** DNS-over-HTTPS、TLS 指纹伪装(Chrome / Firefox / Safari 的 JA3 模板,经本地 MITM 桥接)、双引擎 ECH(加密 SNI)、每应用代理、以及针对受限 SPA 的 CORS 绕过。
|
||||
- **整个构建自包含。** 二进制 AXML/ARSC 修补、权限裁剪、V1/V2/V3 签名、可直接上架 Google Play 的 AAB 导出,全部在 app 内通过 `apksig` 完成 —— 不排远程队列、不需要电脑。
|
||||
- **发布后仍可扩展。** 通过 JS/CSS 模块、Tampermonkey 风格油猴脚本,或 MV3 Chrome 扩展(可在应用内实时搜索 Chrome 网上应用店并安装)给应用补能力,不必重新发布宿主。
|
||||
- **宿主 UI 原生支持 10 种语言。** 中文、English、العربية(RTL)、Português、Español、Français、Deutsch、Русский、日本語、한국어 —— 设置里随时切换;新增界面文案按 10 语维护。
|
||||
|
||||
---
|
||||
|
||||
## 能力速览
|
||||
|
||||
一眼看清里面有什么。每项都对应下方完整能力地图。
|
||||
|
||||
| 方向 | 亮点 |
|
||||
| --- | --- |
|
||||
| **构建目标** | Web · HTML · 前端 · WordPress · Node.js · PHP · Python · Go · 图片 · 视频 · 图库 · 多网站 |
|
||||
| **浏览器引擎** | 默认系统 WebView;可选 GeckoView(Firefox)运行时 |
|
||||
| **网络与反审查** | DoH(7 个服务商)、TLS 指纹伪装 + MITM 桥、ECH、静态/PAC/SOCKS5 代理、CORS 绕过 |
|
||||
| **隐私与加固** | 50+ 维浏览器指纹伪装、资源加密(AES-256-GCM)、反调试、激活码门控 |
|
||||
| **本地运行时** | 原生 Node.js 18.20、PHP 8.4 + Composer 2.10、Python 3.14、官方 Go 1.26、WordPress 7.x over SQLite |
|
||||
| **扩展能力** | 内置模块、`GM_*` 油猴脚本、MV3 Chrome 扩展、Chrome 网上应用店实时搜索 |
|
||||
| **APK/AAB 产物** | 设备端 V1/V2/V3 签名、Google Play AAB 导出(自动改写 targetSdk)、密钥库管理 |
|
||||
| **Agent** | 通过最多 57 个工具全面操控应用:生成、构建、导出、端口/引擎/运行时管理、应用克隆、广告拦截、配置模板等;429/5xx 自动重试 |
|
||||
| **宿主语言** | **10 种界面语言** —— 中文 · English · العربية · Português · Español · Français · Deutsch · Русский · 日本語 · 한국어(阿语 RTL) |
|
||||
|
||||
---
|
||||
|
||||
## 能打包什么
|
||||
|
||||
| 输入 | 输出 | 适合场景 |
|
||||
| --- | --- | --- |
|
||||
| 网站 URL | 基于 WebView 的 APK | 官网、工具、后台、文档、内部系统 |
|
||||
| HTML / 静态前端 | 走 localhost 的 APK | React、Vue、Vite、静态构建、离线 Web 应用 |
|
||||
| Node.js / PHP / Python / Go | 带设备端本地服务的 APK | 小型服务端应用、管理工具、演示、原型 |
|
||||
| WordPress | 本地 PHP + SQLite 承载的 APK | 便携站点、主题/插件演示、本地内容包 |
|
||||
| 图片 / 视频 / 图库 | 媒体型 APK | 相册、课程材料、作品集、离线浏览 |
|
||||
| 多个网站 | 标签/卡片/信息流/抽屉布局 APK | 导航合集、门户、应用集合 |
|
||||
| 已安装 APK | 重命名克隆或桌面快捷方式伪装 | 图标/名称/包名实验、APK 重打包研究 |
|
||||
|
||||
---
|
||||
|
||||
## 完整能力地图
|
||||
|
||||
DruksCode 的开关非常多。下面按使用场景分组,并用可折叠区段保持页面顶部清爽可读。
|
||||
|
||||
<details>
|
||||
<summary><b>🌐 浏览器引擎与网络</b></summary>
|
||||
|
||||
- **双引擎** —— 默认系统 WebView,可选 GeckoView(Firefox)运行时(首次使用时下载)。
|
||||
- **内核风味伪装** —— 对外表现为 Chrome、Edge、Samsung Internet、Firefox 或 Safari 风格,但真实引擎不变。
|
||||
- **桌面模式**、自定义 User-Agent,以及 document-start / end / idle 三种时机的 JS/CSS 注入。
|
||||
- **弹窗处理** —— 当前窗口、外部浏览器、弹窗窗口或直接拦截。
|
||||
- **代理** —— 静态 HTTP/HTTPS/SOCKS5、PAC、身份验证、绕过规则和本地 HTTP-to-SOCKS 桥。
|
||||
- **DNS-over-HTTPS** —— Cloudflare、Google、AdGuard、NextDNS、CleanBrowsing、Quad9、Mullvad,以及自定义 endpoint;支持 strict / automatic 模式。
|
||||
- **ECH(Encrypted Client Hello)** —— 在**两种引擎**上加密 TLS 握手中的 SNI:GeckoView 经 TRR + 自身 ECH 配置;系统 WebView 经本地 MITM 桥的 Chromium 上行链路(首次使用自动下载组件,导出的 APK 内置)。开关启用时自动联动 DoH;与 SOCKS 上游代理互斥。
|
||||
- **TLS 指纹伪装** —— 模拟 Chrome 131 / Firefox 133 / Safari 18 的 JA3 指纹(或自定义密码套件),经本地 TLS-MITM 桥接,让发出的 ClientHello 与真实浏览器一致。
|
||||
- **CORS 绕过** —— 默认开启,让受限静态 SPA 能调用原本被 CORS 拦截的外部 API;同源请求不会被误拦,仅需 CORS/内网桥接时可用轻量 `PrivateNetworkNativeBridgeAdapter`,不必挂完整 Native Bridge。
|
||||
- **回退链** —— 主目标不可达时自动回退到镜像 URL。
|
||||
- **PWA** 离线缓存策略、自定义错误页、每应用 hosts 覆写和支付协议处理。
|
||||
- **兼容开关** —— blob 下载拦截、滚动记忆、图片修复、剪贴板 / 方向 / 通知 polyfill、内网桥接和 Native Bridge 能力门控。
|
||||
- **下载位置** —— 系统 Downloads、应用私有目录,或用户用 SAF 自选文件夹,整条打包透传链已接通。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🛡️ 隐私、指纹防御与加固</b></summary>
|
||||
|
||||
- **跨 50+ 维的浏览器指纹伪装** —— User-Agent、WebGL、Canvas、AudioContext、ClientRects、时区、语言、内存、媒体设备、WebRTC、字体、电池、权限、性能、存储、通知、CSS media、iframe 传播和错误栈清理。
|
||||
- **hosts 规则广告拦截** + cosmetic MutationObserver 过滤,**内置 20 个社区过滤源**(EasyList、uBlock Origin、AdGuard、AdAway + 8 个语言列表),支持逐源启用/停用/删除,自定义导入的过滤源带名称、以卡片管理并可按应用勾选,以及打包进 APK 的自定义订阅规则。
|
||||
- **资源加密**(PBKDF2 + AES-256-GCM)覆盖打包进去的配置、HTML、媒体和 BGM;可设置自定义加密密码,比默认的包名/证书派生密钥更能抵御逆向提取。
|
||||
- **运行时加固**(开启加密后可用)—— 反调试、反 Frida、DEX 篡改检测;威胁响应可选只记录、静默退出或随机崩溃。
|
||||
- **WebView/内容隔离**覆盖存储、WebRTC、Canvas、Audio、WebGL、字体、请求头和 IP 暴露面。
|
||||
- **激活码门控** —— 本地验证,或接入你自己的 HTTPS 接口并用 EC P-256 验签。接口契约见 [remote activation 文档](remote-activation.md)。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>📦 设备端服务器运行时(fork + exec)</b></summary>
|
||||
|
||||
- **Node.js**(18.20.x)跑在独立 `:nodejs` OS 进程中,底层由原生 `node_launcher` 加载 `libnode.so`;支持自定义原生 `.node` 扩展。
|
||||
- **PHP 8.4** 取自 `pmmp/PHP-Binaries`,首次使用时下载,支持 Composer 2.10.x 和自定义原生扩展(`zend_extension`、`.so`)。
|
||||
- **Python 3.14** —— Flask、Django、FastAPI/uvicorn、Tornado、内置 HTTP server;pip 依赖解析到 `.pypackages`,支持自定义原生扩展;二进制名按版本生成,后续升级不必硬编码路径。
|
||||
- **Go 1.26** —— 官方 Linux arm64 工具链(`.tar.gz` 来自 `dl.google.com`,国内走 USTC 镜像),设备端 `go build` / `go mod` / `go run`、`vendor/` 离线构建、静态文件服务和原生 `go_exec_loader` 包装层;DNS 与 CA 信任走与 PHP 相同的本机 JVM 桥接。
|
||||
- **WordPress 7.x** 跑在本地 PHP 之上,用 `sqlite-database-integration` 接 SQLite,支持主题和插件导入。
|
||||
- **Linux Environment** 页面管理 Node、PHP、Python 的工具链和依赖。
|
||||
- **Port Manager** 通过广播 receiver 协调不同生成应用之间的运行时端口。
|
||||
- **本地 DNS 桥接代理**(运行在 Android JVM 的 HTTP CONNECT)在 musl/打包二进制无法访问系统 DNS 时,为运行时提供可用的 DNS 解析和出站 HTTP。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🧩 扩展与自动化</b></summary>
|
||||
|
||||
- **内置模块** —— 视频下载(YouTube / B 站 / 抖音 / 小红书提取器)、带 YouTube 净化(跳过广告、最高画质、后台播放、SponsorBlock)的视频增强、网页分析、页内查找、暗色模式、隐私工具、内容增强、元素拦截和 YouTube 启动器。
|
||||
- **油猴脚本** —— Greasemonkey/Tampermonkey 风格的 `.user.js`,配套 `GM_*` 桥接(存储、请求、样式、菜单命令)和按授权开放的 Promise 风格 `GM.*` API。
|
||||
- **MV3 Chrome 扩展运行时**,支持 manifest 内容脚本在 isolated 或 main world 注入,并提供覆盖 runtime、storage、tabs、scripting 和 declarative network request 解析的 `chrome.*` polyfill。
|
||||
- **应用内 Chrome 网上应用店搜索** —— 按关键词浏览并安装浏览器扩展(也可粘贴商店链接 / 扩展 ID),离线时回退到手动导入。
|
||||
- **分享码**(`DKC1:` gzip + Base64)和 ZXing 二维码传播。
|
||||
- **Agent** —— 工具调用型助手,内置最多 57 个工具覆盖应用全部功能:创建/编辑/构建/导出应用、管理端口与浏览器引擎、安装/清理运行时、广告拦截 hosts 规则、通用配置模板、使用统计、应用克隆、批量导入、Play 合规检查、模块开发。计划模式等待用户确认;对 429/5xx 自动退避重试。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>📱 应用体验</b></summary>
|
||||
|
||||
- **启动屏** —— 图片或视频,支持跳过、视频裁剪区间和固定方向。
|
||||
- **背景音乐** —— 播放列表 + LRC 同步歌词、歌词动画、自定义字体/颜色/描边/阴影和在线音乐搜索。
|
||||
- **工具栏、状态栏(亮色/暗色)、导航栏、悬浮窗模式和长按菜单样式。** 浏览器工具栏是一个主开关(默认关闭),可逐项控制标题/URL/后退/前进/刷新按钮,并带原生 **页内查找** 底栏(工具栏查找按钮)和 **控制台** 面板,便于在设备上调试。状态栏颜色可跟随主题、自定义色、全透明,或 **PAGE_TOP**(采样页面顶部像素,让系统栏跟内容同色)。
|
||||
- **下载位置模式** —— 系统 Downloads、应用私有目录,或用户用 SAF 自选文件夹。
|
||||
- **公告模板**,可在启动、定时或无网络时触发。
|
||||
- **宿主应用语言** —— 整个打包器界面可在 10 种语言间切换(中文 / English / العربية / Português / Español / Français / Deutsch / Русский / 日本語 / 한국어);阿拉伯语完整 RTL。
|
||||
- **页内翻译覆盖层** —— 20 种目标语言,支持 Google、MyMemory、LibreTranslate、Lingva 引擎,并在引擎间自动故障转移(给*生成应用内容*做页内翻译,与宿主 UI 语言是两套能力)。
|
||||
- **打印桥接** —— 拦截 `window.print()` 和 blob/data-URL 的 PDF,交给 Android 打印框架 / 导出 PDF(onPageStarted 会再注入一次,避免晚导航丢 hook)。
|
||||
- **媒体会话桥接** —— 把网页媒体接入系统媒体通知和锁屏控制,支持蓝牙耳机和 Android Auto。
|
||||
- **通知** —— Web Notification polyfill、定时与持久化通知(含进度更新)、URL 轮询前台服务、深链、开机自启、定时启动和后台运行服务。
|
||||
- **每个 APK 的使用统计**、Vico 图表和 URL 健康监测。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🔧 APK / AAB 导出与签名</b></summary>
|
||||
|
||||
- **自定义包名**、`versionName`、`versionCode`、图标、名称、架构目标和导出格式。
|
||||
- **按生成 APK 的实际勾选注入权限**,并从模板 manifest 中裁剪未使用权限。
|
||||
- **一键 AAB 导出** —— 按需自动构建 APK,转换成可直接上架的签名 AAB(自动把 `targetSdk` 改写到 Play 要求的级别,目前为 36,并在本地生成 protobuf 元数据);支持中途取消。
|
||||
- **密钥库管理** —— 创建、导入、导出、删除和证书指纹查看;支持 PKCS12/PFX/JKS/BKS 导入,包括 Android Studio upload key 那种 store 密码和 key 密码不同的情况。
|
||||
- **签名方案** —— V1、V2、V3 独立控制,可对旧证书兼容性自动回退;自定义 V1 签名文件名,对应 `META-INF/<name>.SF` / `.RSA`。
|
||||
- **性能选项** —— 图片压缩、WebP 转换、代码压缩、懒加载、DNS 预取、preload 提示。
|
||||
- **完整项目备份/恢复和应用数据备份/恢复。**
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🗂 文件管理与项目工具</b></summary>
|
||||
|
||||
- **文件管理** —— 一个界面统一查看、分享、安装、打开和清空构建产物(APK 构建、AAB 导出、应用克隆、构建日志)和用户文件目录,并提供只读的构建日志查看器。
|
||||
- **网站爬虫**用于生成离线包 —— HTML、CSS、JS、图片、字体、`url()`、`srcset`、`@import`、路径重写、同域限制、深度限制和体积限制;并行流式 worker 池,进度回调回到主线程。
|
||||
- **多网站应用** —— 标签、卡片、信息流、抽屉布局,每站点独立图标/主题色/提取选择器/刷新间隔和共享 JS/CSS。
|
||||
- **图库应用** —— 媒体分类、网格/列表/时间线视图、随机/单循、排序、缩略图条、浮层、视频自动下一个和播放记忆。
|
||||
- **应用修改器** —— 桌面快捷方式伪装,或真正的二进制克隆、manifest/资源修补和重签名。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🔬 专项工具与研究功能</b></summary>
|
||||
|
||||
- **设备伪装**属于技术演示能力,必须在用户知情同意下使用。
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Agent
|
||||
|
||||
DruksCode 内置了一个 AI Agent(从 **⋮ → Agent** 打开),可以通过自然语言对话操控整个应用。你只需描述想做什么,Agent 就会通过工具调用循环在设备端执行,后端模型由你在 AI 设置中配置的任意受支持接口提供——Chat Completions、Anthropic Messages、OpenAI Responses 三种 API 格式,以及 Google Gemini 与 Ollama / LM Studio / VLLM 本地模型。
|
||||
|
||||
**工作原理:**
|
||||
|
||||
1. 你发送消息(也可附带图片作为视觉上下文)。
|
||||
2. LLM 推理后发出 `tool_calls`。
|
||||
3. Agent 在设备端执行每个工具 —— 只读工具直接运行;写入工具会先弹出权限确认对话框。
|
||||
4. 结果回传给 LLM,LLM 继续执行直到任务完成或向你提出澄清问题。
|
||||
|
||||
**最多 57 个内置工具,按功能域分组(3 个图像工具仅在配置了图像模型时加载):**
|
||||
|
||||
| 功能域 | 示例 |
|
||||
| --- | --- |
|
||||
| 文件 | 读取、写入、编辑、删除、列出、glob、grep 项目文件 |
|
||||
| 应用 | 列出、查看、创建、更新应用配置 |
|
||||
| 应用生命周期 | 构建 APK/AAB、导出、分享、创建快捷方式、复制、删除、移动分类 |
|
||||
| 端口与引擎 | 扫描/终止端口,查看/切换/删除浏览器引擎(WebView、GeckoView) |
|
||||
| 运行时 | Node.js、PHP、Python、Go、WordPress、Linux 环境的状态查看、安装和缓存清理 |
|
||||
| 广告拦截 | 规则数量统计,导入/删除/启用/停用 hosts 订阅 |
|
||||
| 统计与健康 | 使用统计、URL 健康检查 |
|
||||
| 应用修改器 | 列出已安装应用、克隆/换壳、批量导入、导出模板 |
|
||||
| 构建环境与合规 | 初始化 Linux 构建环境、安装组件、Google Play 合规检查 |
|
||||
| 模块 | 列出、创建、更新扩展模块 |
|
||||
| 交互 | 向用户提问(多选)、计划模式(提议→批准→执行)、待办跟踪 |
|
||||
|
||||
---
|
||||
|
||||
## 模块市场
|
||||
|
||||
DruksCode 有一个由 GitHub 驱动的模块市场,用来分发社区贡献的 JS/CSS 扩展模块。目录本质上就是这个仓库里的文件,所以贡献流程就是普通 PR。
|
||||
|
||||
```
|
||||
modules/
|
||||
├── registry.json # App 读取的目录
|
||||
├── submissions.json # CI 生成的 PR / 贡献者元数据
|
||||
├── README.md # 贡献者指南
|
||||
└── <模块文件夹>/ # 每个模块
|
||||
```
|
||||
|
||||
App 会同时拉取 `registry.json` 和 `submissions.json`,只展示两边都存在的模块,保证应用内市场和已经合并的 PR 对齐。submissions 文件还会记录每个模块的全部贡献者,因此应用内会以叠加头像的形式展示所有参与过该模块的人,并按贡献模块数排出贡献者榜单。目录文件和模块图标会优先走全球镜像加速,raw.githubusercontent.com 和 jsDelivr 作为自动回退,因此商店在全球(含中国大陆)都能快速加载。
|
||||
|
||||
- 用户打开 **扩展模块** 页面,点击右上角商店图标即可安装。
|
||||
- 贡献者在 `modules/` 下添加文件夹,更新 `registry.json`,然后提交 PR。
|
||||
- 客户端默认缓存 1 小时,模块合并后不需要发新版 App。
|
||||
|
||||
这里保留的是模块市场的高层说明;真正的投稿规则、字段 schema、审核 Checklist 和 CI 校验细节统一写在 [`modules/README.md`](../../modules/README.md)。
|
||||
|
||||
社区市场只承载 JS/CSS 扩展模块。**浏览器扩展(MV3)**不再是社区目录 —— **浏览器扩展** Tab 直接实时搜索 Chrome 网上应用店:输入关键词、浏览结果、通过现有的 CRX 下载链路按需安装。如果实时搜索不可达,也可以粘贴商店链接或 32 位扩展 ID 直接安装。实时搜索需要能访问 Google 的网络。
|
||||
|
||||
## 文档站点
|
||||
|
||||
官方文档站点发布在 **[shiaho777.github.io/web-to-app](https://shiaho777.github.io/web-to-app/)**,中英双语:
|
||||
|
||||
| 板块 | 地址 | 内容 |
|
||||
| --- | --- | --- |
|
||||
| 使用指南 | [/zh/guide/introduction](https://shiaho777.github.io/web-to-app/zh/guide/introduction) | 快速上手、主界面、应用类型、单应用操作与通用配置、FAQ |
|
||||
| 开发者文档 | [/zh/developer/](https://shiaho777.github.io/web-to-app/zh/developer/) | 架构、导出管线、shell 同步、配置漂移、i18n、常见改动配方 |
|
||||
| 扩展开发 | [/zh/extensions/](https://shiaho777.github.io/web-to-app/zh/extensions/) | JS/CSS 模块、油猴脚本、Chrome MV3、API 参考、发布流程 |
|
||||
| English | [/](https://shiaho777.github.io/web-to-app/) | 上述全部页面的英文原版 |
|
||||
|
||||
## 架构说明
|
||||
|
||||
- 仓库有**三个 Gradle 模块**:`app`(完整构建器和宿主)、`shell`(嵌入生成 APK 的运行时宿主)、`clone-host`(应用克隆的宿主代码 —— 编译提取 `classes.jar`,经 d8 转 DEX,作为 asset 供 `AppCloner` 使用)。
|
||||
- 运行时代码以 `app` 为唯一事实来源,再同步到 `shell`,所以共享 WebView/运行时行为只维护一份(`core/shell`、`core/webview`、`core/engine`、`core/extension`、`ui/shell` 等)。
|
||||
- APK 构建器在二进制 AXML/ARSC 层修补模板 APK,注入配置与资源,裁剪权限,并用 `apksig` 签名。另有独立的加密构建路径(`EncryptedApkBuilder`)提供资源加密、加壳和完整性校验。
|
||||
- 生成 APK(经 shell 模板)特意把 `targetSdk` 钉在 28 —— 这是让它们能从 app 存储 `fork`、`exec` 原生运行时(Node.js、PHP、Python、Go、WordPress)的关键,网址转 APK 类工具做不到这点。宿主应用自身以 35 为目标(杀毒引擎会把低 targetSdk 构建误判为旧木马);该级别的 SELinux W^X 会拦截宿主侧基于 exec 的运行时预览,它们会以明确提示优雅降级 —— Node.js 预览(JNI)和所有导出的应用均不受影响。导出 AAB 时会单独把 `targetSdk` 改写以满足 Google Play 上架要求;纯 WebView 应用类型(Web/HTML/Frontend/Gallery/Media/MultiWeb)还可在 APK 导出面板选择提高独立 APK 的 `targetSdk`(34/35/36),服务端运行时类型固定 28,因为 `targetSdk >= 29` 的 W^X 会破坏内置二进制的 fork+exec。
|
||||
- 服务端运行时和可选 GeckoView 原生库(`.so` + `omni.ja`)不会打进基础 APK,而是在首次使用时下载;GeckoView 的 API 类来自 gradle 依赖,而体积大的原生制品按需拉取。
|
||||
- 配置中心是 `WebApp`(`data/model/WebApp.kt`)及其各 `*Config` 类 —— 所有功能配置的单一事实来源,经一条完整的打包透传链带进生成的 APK。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Kotlin、Jetpack Compose、Material 3
|
||||
- Koin 依赖注入
|
||||
- Room 2.7.2 + KSP 数据持久化
|
||||
- OkHttp 4.12.0 + `okhttp-dnsoverhttps`
|
||||
- `com.android.tools.build:apksig` 8.3.0 用于 APK 签名
|
||||
- BouncyCastle 1.78.1 用于加密与签名
|
||||
- `protobuf-javalite` 3.25.5 用于 AAB 元数据
|
||||
- Firebase Cloud Messaging(需自行配置 Firebase)用于推送通知
|
||||
- GeckoView 作为可选浏览器引擎(原生库在首次使用时下载)
|
||||
- Coil 负责图片、视频、GIF 加载
|
||||
- Haze 用于毛玻璃 / 玻璃拟态 UI
|
||||
- Gson 用于 JSON 序列化(shell 配置链路)
|
||||
- AndroidX Security Crypto + DataStore 存储密钥
|
||||
- Vico Compose-M3 绘制图表
|
||||
- ZXing 用于二维码分享
|
||||
- Apache Commons Compress + xz 用于项目导入和网站爬虫
|
||||
- JNI 原生 C++ 目标:`node_launcher` 和 `go_exec_loader`
|
||||
- Robolectric 单元测试
|
||||
|
||||
完整依赖见 [app/build.gradle.kts](../../app/build.gradle.kts)。
|
||||
|
||||
## 从源码构建
|
||||
|
||||
要求:Android Studio Hedgehog 或更新版本,JDK 17。Gradle wrapper 已锁定 Gradle 9.4.1。
|
||||
|
||||
```bash
|
||||
git clone https://github.com/shiaho777/web-to-app.git
|
||||
cd drukscode
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
|
||||
Release 构建请通过 `local.properties` 和 `app/build.gradle.kts` 配置签名。
|
||||
|
||||
## 参与贡献
|
||||
|
||||
| 路径 | 内容 | 指南 |
|
||||
| --- | --- | --- |
|
||||
| `modules/` | 给应用内市场提交一个社区模块 | [modules/README.md](../../modules/README.md) |
|
||||
| Issues | 报告 Bug 或申请功能 | [GitHub Issues](https://github.com/shiaho777/web-to-app/issues) |
|
||||
| 代码 | 修 Bug 或在 Android 客户端做新功能 | [CONTRIBUTING.md](../CONTRIBUTING.md) |
|
||||
| `AGENTS.md` | 面向 AI 编码助手与深度贡献者的权威指南(架构连线、shell 同步、导出流水线、配置漂移、打包约束) | [AGENTS.md](../../AGENTS.md) |
|
||||
|
||||
## 联系方式
|
||||
|
||||
开发者:**shiaho**。
|
||||
|
||||
| 平台 | 链接 |
|
||||
| --- | --- |
|
||||
| GitHub | [github.com/shiaho777/web-to-app](https://github.com/shiaho777/web-to-app) |
|
||||
| Telegram | [t.me/webtoapp777](https://t.me/webtoapp777) |
|
||||
| X (Twitter) | [@shiaho777](https://x.com/shiaho777) |
|
||||
| Bilibili | [b23.tv/8mGDo2N](https://b23.tv/8mGDo2N) |
|
||||
| QQ 群 | 1041130206 |
|
||||
|
||||
## 许可证
|
||||
|
||||
[The Unlicense](../../LICENSE)。
|
||||
|
||||
设备伪装等高级功能仅用于技术演示,必须在用户知情同意下使用。
|
||||
|
||||
## 鸣谢
|
||||
|
||||
[daoxe.com](https://daoxe.com) — 自用中转站。
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.swiftproxy.net/?ref=shiaho">
|
||||
<img src="../assets/sponsors/swiftproxy-banner-zh.png" width="640" alt="Swiftproxy —— 全球高质量纯净住宅IP,动态流量不过期,支持HTTP(S)/SOCKS5,九折优惠码:PROXY90" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.swiftproxy.net/?ref=shiaho"><b>由 Swiftproxy 赞助</b></a> —— Swiftproxy 提供 9000万+ 纯净住宅 IP,覆盖全球 220+ 个国家和地区,支持 HTTP(S)/SOCKS5、动态轮换、Sticky Session 及精准地域定位。适用于 Web App、自动化、网页抓取及全球网络访问等场景,提供稳定可靠的代理服务。支持免费测试,使用优惠码 <code>PROXY90</code> 可享 9 折优惠。
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**开源 · 为 Android 高阶用户打造 · Star 一下支持项目**
|
||||
|
||||
</div>
|
||||
331
.github/docs/remote-activation.md
vendored
Normal file
331
.github/docs/remote-activation.md
vendored
Normal file
@ -0,0 +1,331 @@
|
||||
# Remote Activation — Server Contract
|
||||
|
||||
DruksCode can verify activation codes against **your own HTTPS endpoint**
|
||||
instead of (or in addition to) the codes baked into the APK. This lets you
|
||||
revoke codes, issue them dynamically, and control usage centrally — without
|
||||
rebuilding the app.
|
||||
|
||||
> **Security note.** The host is open source and verification runs on the
|
||||
> client, so a determined attacker can still patch the check out. Remote
|
||||
> verification *raises the bar* and gives you revocation/rotation; it is not
|
||||
> an anti-cracking guarantee. The response signature below stops a fake server
|
||||
> or a man-in-the-middle from forging an `ok: true`, which is the part that
|
||||
> matters most.
|
||||
|
||||
## How it is configured in the app
|
||||
|
||||
When building an app (or in App Modifier), enable **Online Verification** under
|
||||
the activation-code section and fill in:
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| Verification endpoint | Your `https://…` URL. Plain `http://` is rejected. |
|
||||
| Signature public key | An **EC P-256** public key in Base64 (SPKI / `BEGIN PUBLIC KEY`). The app verifies every response with it. |
|
||||
| Offline policy | `ALLOW_CACHED` (default): allow the last successful result until it expires. `DENY`: block when offline. `ALLOW`: always allow when offline (insecure). |
|
||||
| Deliver target URL | When on, the target URL is delivered by the server at activation time (not packaged in the APK). See [Dynamic URL delivery](#dynamic-url-delivery). |
|
||||
| Encrypt target URL | When on (requires Deliver URL), the server must encrypt the URL with AES-256-GCM before signing. See [URL Encryption](#url-encryption-aes-256-gcm). |
|
||||
| AES-256 key | 32-byte Base64 key shared with the server. Required when encryption is enabled. |
|
||||
|
||||
## Request (app → your server)
|
||||
|
||||
```
|
||||
POST <your verification endpoint>
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "ABC123",
|
||||
"deviceId": "f3a9…",
|
||||
"packageName": "com.example.app",
|
||||
"nonce": "Base64-random-24-bytes",
|
||||
"ts": 1733270400000,
|
||||
"deviceBound": false
|
||||
}
|
||||
```
|
||||
|
||||
- `code` is normalised (uppercased, trimmed) before sending.
|
||||
- `deviceId` is a per-device identifier (not a hardware ID; it is a salted hash).
|
||||
- `nonce` is fresh per request — you **must** echo it back unchanged (replay
|
||||
protection).
|
||||
- `ts` is the client clock in epoch milliseconds.
|
||||
- `deviceBound` is `true` when the app has the **Device binding (one-time codes)**
|
||||
toggle enabled in the editor's remote-activation section (it is always `false`
|
||||
for purely local verification — see below). For such codes your server should
|
||||
enforce per-device binding: record the first `deviceId` that activates a given
|
||||
`code`, and reject the same `code` from any different `deviceId` (return
|
||||
`{ "ok": false }`). This is the only way to truly restrict a device-bound code
|
||||
to one device — the app itself has no shared state between devices, so a purely
|
||||
local device-bound code can only prevent re-activation on the same device
|
||||
after a local reset or hardware change.
|
||||
|
||||
The reference worker in `examples/remote-activation-worker` implements this as
|
||||
seats: `maxDevices` (default `1`) devices may claim a code; a code with
|
||||
`maxDevices: 1` behaves as a **one-time / single-device code**. The claimed
|
||||
device id survives uninstall + reinstall on the same device (it derives from
|
||||
`ANDROID_ID`, stable per signing key), so a seat is only released when you edit
|
||||
the KV record. To free a seat manually, remove the `deviceId` entry from
|
||||
`record.devices` (or delete `code:<CODE>`).
|
||||
|
||||
## Response (your server → app)
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"expiresAt": 1735862400000,
|
||||
"remainingUses": 5,
|
||||
"message": "",
|
||||
"nonce": "Base64-random-24-bytes",
|
||||
"sig": "Base64-ECDSA-signature",
|
||||
"url": "https://example.com/app"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `ok` | boolean | `true` grants activation. |
|
||||
| `expiresAt` | number \| null | Epoch ms. If present and in the past, the app treats it as expired. Use `0`/omit for "never expires". |
|
||||
| `remainingUses` | number \| null | Informational. Use `-1`/omit when not tracking. |
|
||||
| `message` | string | Shown to the user on rejection. |
|
||||
| `nonce` | string | Must equal the request nonce. |
|
||||
| `sig` | string | Base64 ECDSA (`SHA256withECDSA`, DER-encoded) over the canonical payload below. |
|
||||
| `url` | string \| null | Optional. The target URL to load. Only used (and signature-bound) when the app has **Deliver target URL from server** enabled — see [Dynamic URL delivery](#dynamic-url-delivery). When **Encrypt target URL** is also enabled, this field contains the AES-256-GCM ciphertext (see [URL Encryption](#url-encryption-aes-256-gcm)). Omit otherwise. |
|
||||
|
||||
### Canonical payload that gets signed
|
||||
|
||||
The app rebuilds this exact JSON string and verifies `sig` against it. **Key
|
||||
order and formatting matter** — it is compact JSON with these four keys, in
|
||||
this order:
|
||||
|
||||
```
|
||||
{"ok":<true|false>,"expiresAt":<number>,"remainingUses":<number>,"nonce":"<nonce>"}
|
||||
```
|
||||
|
||||
- `expiresAt` falls back to `0` when you omit it.
|
||||
- `remainingUses` falls back to `-1` when you omit it.
|
||||
- No spaces, booleans unquoted, numbers unquoted.
|
||||
|
||||
When **Deliver target URL from server** is enabled, a fifth key `url` is
|
||||
appended (after `nonce`), and the signature must cover it too:
|
||||
|
||||
```
|
||||
{"ok":<true|false>,"expiresAt":<number>,"remainingUses":<number>,"nonce":"<nonce>","url":"<url>"}
|
||||
```
|
||||
|
||||
`url` falls back to `""` when you omit it. Apps that do not enable URL delivery
|
||||
verify the legacy four-key payload, so existing servers keep working unchanged.
|
||||
|
||||
Example of the precise bytes to sign:
|
||||
|
||||
```
|
||||
{"ok":true,"expiresAt":1735862400000,"remainingUses":5,"nonce":"k7Q…"}
|
||||
```
|
||||
|
||||
## Dynamic URL delivery
|
||||
|
||||
When **Deliver target URL from server** is enabled in the app, the target URL
|
||||
is not packaged into the APK. Instead the app loads the URL you return in the
|
||||
`url` field after a successful activation (and caches it for offline launches
|
||||
under `ALLOW_CACHED`). This lets you change the URL without repackaging, and
|
||||
keeps the URL out of the APK so it cannot be extracted statically.
|
||||
|
||||
- Return the URL in `url` and include it in the signed payload (fifth key).
|
||||
- The URL is cached on the device after a successful online activation; offline
|
||||
launches reuse the cached URL per the offline policy.
|
||||
- The URL is delivered in the clear inside the signed response (over HTTPS).
|
||||
This raises the bar against casual extraction but is not DRM-grade — a
|
||||
determined attacker can still recover it from the response or memory.
|
||||
|
||||
## URL Encryption (AES-256-GCM)
|
||||
|
||||
When both **Deliver target URL from server** and **Encrypt target URL** are
|
||||
enabled, the server must encrypt the URL with AES-256-GCM **before** signing.
|
||||
The ciphertext goes into the `url` field and the signed payload.
|
||||
|
||||
| Parameter | Value |
|
||||
| --- | --- |
|
||||
| Algorithm | AES-256-GCM (authenticated encryption) |
|
||||
| Key | 32 bytes, shared between app config and server |
|
||||
| IV | 12 random bytes per encryption |
|
||||
| Wire format | `Base64(IV[12B] \|\| ciphertext \|\| GCM tag[16B])` |
|
||||
|
||||
**Why encrypt the URL when the response is already over HTTPS and ECDSA-signed?**
|
||||
Defense in depth. If an attacker compromises the TLS layer (corporate MITM
|
||||
proxy with a user-installed CA, misconfigured certificate pinning bypass) or
|
||||
dumps the response from memory, the URL stays unreadable without the AES key.
|
||||
The signature still prevents tampering, and the encryption prevents reading.
|
||||
|
||||
### Generating an AES-256 key
|
||||
|
||||
```bash
|
||||
# Generate a 32-byte random key and Base64-encode it
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
Paste the same Base64 string into both the app's **AES-256 key** field and
|
||||
your server configuration.
|
||||
|
||||
### Server-side encryption (Node.js)
|
||||
|
||||
```js
|
||||
const crypto = require("crypto");
|
||||
|
||||
function encryptUrl(plaintext, aesKeyBase64) {
|
||||
const key = Buffer.from(aesKeyBase64, "base64"); // 32 bytes
|
||||
const iv = crypto.randomBytes(12);
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||
let encrypted = cipher.update(plaintext, "utf8");
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||
const tag = cipher.getAuthTag(); // 16 bytes
|
||||
// Wire format: IV || ciphertext || tag
|
||||
return Buffer.concat([iv, encrypted, tag]).toString("base64");
|
||||
}
|
||||
```
|
||||
|
||||
### Signing flow with encryption
|
||||
|
||||
1. Encrypt the URL → Base64 ciphertext blob.
|
||||
2. Build the canonical payload with the ciphertext blob as the `url` value.
|
||||
3. Sign the canonical payload with ECDSA.
|
||||
4. Return the ciphertext blob in the response `url` field and the signature in `sig`.
|
||||
|
||||
The client verifies the signature first (which covers the ciphertext), then
|
||||
decrypts `url` with the shared AES key to recover the plaintext URL. An
|
||||
attacker who tampers with the ciphertext breaks the signature; an attacker who
|
||||
reads the response sees only encrypted bytes.
|
||||
|
||||
## Generating a key pair
|
||||
|
||||
```bash
|
||||
# private key (keep on your server)
|
||||
openssl ecparam -name prime256v1 -genkey -noout -out ec_private.pem
|
||||
# public key (paste into the app's "Signature public key" field, header lines optional)
|
||||
openssl ec -in ec_private.pem -pubout -out ec_public.pem
|
||||
```
|
||||
|
||||
## Ready-to-deploy option (Cloudflare Worker)
|
||||
|
||||
If you would rather not run a server at all, there is a complete reference
|
||||
worker in
|
||||
[`examples/remote-activation-worker/`](../../examples/remote-activation-worker/README.md):
|
||||
codes held in Workers KV (revocable without rebuilding the APK), signed
|
||||
responses, device binding, optional AES-256-GCM URL delivery, and a
|
||||
`GET /` self-check that hands you the public key to paste into the app. It
|
||||
deploys with `npx wrangler deploy` and ships a test that verifies responses the
|
||||
way the client does.
|
||||
|
||||
One caveat it documents, because it applies to any server you write: **the
|
||||
request does not say whether the app expects a delivered URL**, so the server
|
||||
cannot discover it. A code that carries a `url` must be paired with an app that
|
||||
has "Deliver target URL" enabled, and a code without one with an app that has it
|
||||
disabled — otherwise the two sides sign different payloads and every activation
|
||||
fails.
|
||||
|
||||
The Node example below is the same contract for your own infrastructure.
|
||||
|
||||
## Reference server (Node.js, no framework)
|
||||
|
||||
```js
|
||||
const http = require("http");
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
|
||||
const privateKey = crypto.createPrivateKey(fs.readFileSync("ec_private.pem"));
|
||||
|
||||
// Your own source of truth. Revoke by removing/flipping entries here.
|
||||
// `url` is optional: set it (and enable "Deliver target URL from server" in the
|
||||
// app) to deliver the target URL dynamically instead of packaging it.
|
||||
// When URL encryption is enabled, encrypt `url` with the shared AES key before
|
||||
// returning it (see the `encryptUrl` helper below).
|
||||
const CODES = {
|
||||
"ABC123": { expiresAt: 1735862400000, remainingUses: 5, url: "https://example.com/app" },
|
||||
};
|
||||
|
||||
// ---- AES-256-GCM encryption (only needed when "Encrypt target URL" is on) ----
|
||||
const AES_KEY_BASE64 = process.env.AES_KEY_BASE64 || ""; // same as in the app config
|
||||
const AES_KEY = AES_KEY_BASE64 ? Buffer.from(AES_KEY_BASE64, "base64") : null;
|
||||
|
||||
function encryptUrl(plaintext) {
|
||||
if (!AES_KEY || AES_KEY.length !== 32) return plaintext; // passthrough if not configured
|
||||
const iv = crypto.randomBytes(12);
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", AES_KEY, iv);
|
||||
let encrypted = cipher.update(plaintext, "utf8");
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||
const tag = cipher.getAuthTag();
|
||||
return Buffer.concat([iv, encrypted, tag]).toString("base64");
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
function signedPayload({ ok, expiresAt, remainingUses, nonce, url }) {
|
||||
// Must match the app's canonical order exactly.
|
||||
const payload = {
|
||||
ok,
|
||||
expiresAt: expiresAt ?? 0,
|
||||
remainingUses: remainingUses ?? -1,
|
||||
nonce,
|
||||
};
|
||||
// Include url only when delivering it; it becomes the fifth signed key.
|
||||
// When encryption is on, `url` is already the ciphertext blob here.
|
||||
if (url !== undefined) {
|
||||
payload.url = url ?? "";
|
||||
}
|
||||
return JSON.stringify(payload);
|
||||
}
|
||||
|
||||
function sign(payloadString) {
|
||||
return crypto
|
||||
.sign("sha256", Buffer.from(payloadString, "utf8"), privateKey)
|
||||
.toString("base64");
|
||||
}
|
||||
|
||||
http
|
||||
.createServer((req, res) => {
|
||||
let body = "";
|
||||
req.on("data", (c) => (body += c));
|
||||
req.on("end", () => {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(body);
|
||||
} catch {
|
||||
res.writeHead(400);
|
||||
return res.end();
|
||||
}
|
||||
|
||||
const code = String(parsed.code || "").trim().toUpperCase();
|
||||
const nonce = String(parsed.nonce || "");
|
||||
const entry = CODES[code];
|
||||
|
||||
const result = entry
|
||||
? { ok: true, expiresAt: entry.expiresAt, remainingUses: entry.remainingUses, message: "" }
|
||||
: { ok: false, expiresAt: 0, remainingUses: -1, message: "Code not recognised" };
|
||||
|
||||
// Encrypt the URL if AES key is configured; the ciphertext goes into
|
||||
// both the signed payload and the response.
|
||||
const urlPlain = entry && entry.url ? entry.url : undefined;
|
||||
const urlEnc = urlPlain ? encryptUrl(urlPlain) : undefined;
|
||||
|
||||
const sig = sign(signedPayload({ ...result, nonce, url: urlEnc }));
|
||||
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ...result, nonce, sig, url: urlEnc }));
|
||||
});
|
||||
})
|
||||
.listen(8443);
|
||||
```
|
||||
|
||||
Serve it over HTTPS (behind a reverse proxy with a TLS cert, or with
|
||||
`https.createServer`). The app refuses non-HTTPS endpoints.
|
||||
|
||||
## Offline behaviour
|
||||
|
||||
After a successful online check, the app caches the result (bound to the code
|
||||
and its `expiresAt`). With `ALLOW_CACHED`, a later offline launch is allowed
|
||||
until that expiry, then it prompts again. With `DENY`, any offline launch is
|
||||
blocked. With `ALLOW`, offline launches always pass — only use this if losing
|
||||
the gate offline is acceptable.
|
||||
|
||||
## Privacy
|
||||
|
||||
When online verification is on, the app sends the activation code and a device
|
||||
identifier to the endpoint you configure. Disclose this to your users.
|
||||
278
.github/scripts/audit_ui_design_system.py
vendored
Normal file
278
.github/scripts/audit_ui_design_system.py
vendored
Normal file
@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit Compose UI files for design-system migration debt.
|
||||
|
||||
This script is intentionally non-failing by default. It reports legacy patterns
|
||||
so migrations can reduce the count over time without blocking unrelated work.
|
||||
Use --enforce-baseline with .github/scripts/ui_design_allowlist.txt to block new debt
|
||||
while allowing documented historical debt to remain during migration.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from collections import Counter, defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
DEFAULT_TARGETS = (
|
||||
REPO_ROOT / "app/src/main/java/br/com/drukstech/codeapp/ui/screens",
|
||||
REPO_ROOT / "app/src/main/java/br/com/drukstech/codeapp/ui/components",
|
||||
)
|
||||
DEFAULT_ALLOWLIST = REPO_ROOT / ".github/scripts/ui_design_allowlist.txt"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Rule:
|
||||
key: str
|
||||
label: str
|
||||
pattern: re.Pattern[str]
|
||||
|
||||
|
||||
RULES = (
|
||||
Rule(
|
||||
key="hardcoded_color",
|
||||
label="Direct Color(0x...)",
|
||||
pattern=re.compile(r"\bColor\s*\(\s*0x[0-9A-Fa-f_]+"),
|
||||
),
|
||||
Rule(
|
||||
key="raw_corner_shape",
|
||||
label="Raw RoundedCornerShape(dp)",
|
||||
pattern=re.compile(r"\bRoundedCornerShape\s*\(\s*\d+(?:\.\d+)?\.dp"),
|
||||
),
|
||||
Rule(
|
||||
key="raw_card",
|
||||
label="Raw Card/ElevatedCard/OutlinedCard",
|
||||
pattern=re.compile(r"(?<!Enhanced)(?<!Dkc)\b(?:Card|ElevatedCard|OutlinedCard)\s*\("),
|
||||
),
|
||||
Rule(
|
||||
key="raw_surface",
|
||||
label="Raw Surface",
|
||||
pattern=re.compile(r"(?<!Dkc)\bSurface\s*\("),
|
||||
),
|
||||
Rule(
|
||||
key="raw_button",
|
||||
label="Raw Button",
|
||||
pattern=re.compile(r"(?<!Icon)(?<!Text)(?<!Outlined)(?<!Tonal)(?<!Premium)(?<!Dkc)\bButton\s*\("),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def iter_kotlin_files(target: Path) -> list[Path]:
|
||||
if target.is_file():
|
||||
return [target]
|
||||
return sorted(target.rglob("*.kt"))
|
||||
|
||||
|
||||
def resolve_target(target_text: str) -> Path:
|
||||
target = Path(target_text)
|
||||
if not target.is_absolute():
|
||||
target = REPO_ROOT / target
|
||||
return target
|
||||
|
||||
|
||||
def display_targets(targets: list[Path]) -> str:
|
||||
labels = []
|
||||
for target in targets:
|
||||
labels.append(
|
||||
str(target.relative_to(REPO_ROOT))
|
||||
if target.is_relative_to(REPO_ROOT)
|
||||
else str(target)
|
||||
)
|
||||
return ", ".join(labels)
|
||||
|
||||
|
||||
def strip_line_comment(line: str) -> str:
|
||||
return line.split("//", 1)[0]
|
||||
|
||||
|
||||
def audit_file(path: Path) -> dict[str, list[int]]:
|
||||
findings: dict[str, list[int]] = defaultdict(list)
|
||||
try:
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
except UnicodeDecodeError:
|
||||
lines = path.read_text(errors="ignore").splitlines()
|
||||
|
||||
for line_no, line in enumerate(lines, start=1):
|
||||
candidate = strip_line_comment(line)
|
||||
for rule in RULES:
|
||||
if rule.pattern.search(candidate):
|
||||
findings[rule.key].append(line_no)
|
||||
return findings
|
||||
|
||||
|
||||
def relative_repo_path(path: Path) -> str:
|
||||
if path.is_relative_to(REPO_ROOT):
|
||||
return path.relative_to(REPO_ROOT).as_posix()
|
||||
return path.as_posix()
|
||||
|
||||
|
||||
def load_allowlist(path: Path) -> dict[str, Counter[str]]:
|
||||
allowlist: dict[str, Counter[str]] = defaultdict(Counter)
|
||||
if not path.exists():
|
||||
return allowlist
|
||||
|
||||
for line_no, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
parts = line.split("|")
|
||||
if len(parts) != 3:
|
||||
raise ValueError(f"Invalid allowlist line {line_no}: {raw_line}")
|
||||
rel_path, rule_key, count_text = parts
|
||||
if rule_key not in {rule.key for rule in RULES}:
|
||||
raise ValueError(f"Unknown rule '{rule_key}' on allowlist line {line_no}")
|
||||
try:
|
||||
count = int(count_text)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"Invalid count '{count_text}' on allowlist line {line_no}") from exc
|
||||
if count < 0:
|
||||
raise ValueError(f"Negative count on allowlist line {line_no}")
|
||||
allowlist[rel_path][rule_key] = count
|
||||
return allowlist
|
||||
|
||||
|
||||
def write_allowlist(path: Path, current_counts: dict[str, Counter[str]]) -> None:
|
||||
lines = [
|
||||
"# DruksCode UI design-system historical debt baseline.",
|
||||
"# Format: relative/path.kt|rule_key|allowed_count",
|
||||
"# Regenerate only after intentionally accepting a new migration checkpoint.",
|
||||
]
|
||||
for rel_path in sorted(current_counts):
|
||||
counts = current_counts[rel_path]
|
||||
for rule in RULES:
|
||||
count = counts[rule.key]
|
||||
if count > 0:
|
||||
lines.append(f"{rel_path}|{rule.key}|{count}")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def find_baseline_violations(
|
||||
current_counts: dict[str, Counter[str]],
|
||||
allowlist: dict[str, Counter[str]],
|
||||
) -> list[tuple[str, str, int, int]]:
|
||||
violations: list[tuple[str, str, int, int]] = []
|
||||
for rel_path in sorted(current_counts):
|
||||
counts = current_counts[rel_path]
|
||||
allowed = allowlist.get(rel_path, Counter())
|
||||
for rule in RULES:
|
||||
current = counts[rule.key]
|
||||
baseline = allowed[rule.key]
|
||||
if current > baseline:
|
||||
violations.append((rel_path, rule.key, current, baseline))
|
||||
return violations
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
help="Files or directories to scan. Defaults to app ui/screens and ui/components.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fail-on-findings",
|
||||
action="store_true",
|
||||
help="Exit with status 1 if any finding is detected.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enforce-baseline",
|
||||
action="store_true",
|
||||
help="Exit with status 1 only when current findings exceed the allowlist baseline.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--allowlist",
|
||||
default=str(DEFAULT_ALLOWLIST),
|
||||
help="Baseline allowlist file for --enforce-baseline.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--write-allowlist",
|
||||
help="Write the current finding counts as an allowlist baseline.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--top",
|
||||
type=int,
|
||||
default=20,
|
||||
help="Number of highest-debt files to print.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
targets = [resolve_target(target) for target in args.targets] if args.targets else list(DEFAULT_TARGETS)
|
||||
|
||||
files = sorted(
|
||||
{file_path.resolve() for target in targets for file_path in iter_kotlin_files(target)}
|
||||
)
|
||||
totals: Counter[str] = Counter()
|
||||
per_file: list[tuple[Path, Counter[str], dict[str, list[int]]]] = []
|
||||
current_counts: dict[str, Counter[str]] = {}
|
||||
|
||||
for file_path in files:
|
||||
findings = audit_file(file_path)
|
||||
counts = Counter({key: len(lines) for key, lines in findings.items()})
|
||||
if counts:
|
||||
totals.update(counts)
|
||||
per_file.append((file_path, counts, findings))
|
||||
current_counts[relative_repo_path(file_path)] = counts
|
||||
|
||||
print("DruksCode UI design-system audit")
|
||||
print(f"Targets: {display_targets(targets)}")
|
||||
print(f"Files scanned: {len(files)}")
|
||||
print()
|
||||
|
||||
if args.write_allowlist:
|
||||
write_path = Path(args.write_allowlist)
|
||||
if not write_path.is_absolute():
|
||||
write_path = REPO_ROOT / write_path
|
||||
write_allowlist(write_path, current_counts)
|
||||
print(f"Allowlist written: {relative_repo_path(write_path)}")
|
||||
print()
|
||||
|
||||
if not totals:
|
||||
print("No findings.")
|
||||
return 0
|
||||
|
||||
print("Totals:")
|
||||
for rule in RULES:
|
||||
print(f" {rule.label}: {totals[rule.key]}")
|
||||
|
||||
print()
|
||||
print(f"Top {args.top} files:")
|
||||
per_file.sort(key=lambda item: sum(item[1].values()), reverse=True)
|
||||
for file_path, counts, findings in per_file[: args.top]:
|
||||
rel = file_path.relative_to(REPO_ROOT)
|
||||
total = sum(counts.values())
|
||||
detail = ", ".join(
|
||||
f"{rule.key}={counts[rule.key]}"
|
||||
for rule in RULES
|
||||
if counts[rule.key]
|
||||
)
|
||||
first_lines = []
|
||||
for rule in RULES:
|
||||
lines = findings.get(rule.key)
|
||||
if lines:
|
||||
first_lines.append(f"{rule.key}@{','.join(map(str, lines[:5]))}")
|
||||
print(f" {rel}: {total} ({detail})")
|
||||
print(f" first: {'; '.join(first_lines)}")
|
||||
|
||||
if args.enforce_baseline:
|
||||
allowlist_path = Path(args.allowlist)
|
||||
if not allowlist_path.is_absolute():
|
||||
allowlist_path = REPO_ROOT / allowlist_path
|
||||
allowlist = load_allowlist(allowlist_path)
|
||||
violations = find_baseline_violations(current_counts, allowlist)
|
||||
print()
|
||||
if violations:
|
||||
print("Baseline enforcement failed:")
|
||||
for rel_path, rule_key, current, baseline in violations[:50]:
|
||||
print(f" {rel_path}|{rule_key}: current={current}, allowed={baseline}")
|
||||
return 1
|
||||
print("Baseline enforcement passed.")
|
||||
|
||||
return 1 if args.fail_on_findings else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
664
.github/scripts/ci/generate_submissions.py
vendored
Normal file
664
.github/scripts/ci/generate_submissions.py
vendored
Normal file
@ -0,0 +1,664 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate `modules/submissions.json` for the DruksCode Module Market.
|
||||
|
||||
Why: the in-app market only renders modules that have a corresponding
|
||||
submission entry in this file. The repository contract is that an entry
|
||||
is published only when the module has actually landed on `main`, either
|
||||
by a merged PR (the normal path) or by a maintainer direct push (rare,
|
||||
used for seeding the initial example modules).
|
||||
|
||||
This script is what enforces the "merged PRs only" promise. It walks
|
||||
every module folder under `modules/`, asks `git log` for the commit that
|
||||
introduced or last touched it, then queries the GitHub REST API for the
|
||||
PR associated with that commit. If the PR is found and merged, we record
|
||||
PR number, merge timestamp, and the merger's GitHub identity. If no PR
|
||||
is found, the commit is taken as a direct push and we record the
|
||||
commit's author timestamp and resolved GitHub login — any human author
|
||||
is recorded (modules only reach `main` through review or a maintainer
|
||||
push; hiding non-maintainer authors made real contributor modules
|
||||
disappear from the in-app market). Only bot identities
|
||||
(github-actions[bot], web-flow, dependabot[bot]) are excluded, with the
|
||||
repo owner as the fallback when a commit email maps to no GitHub
|
||||
account. An orphan folder added without a commit ends up unrecorded,
|
||||
and the in-app market hides it.
|
||||
|
||||
Run locally (no network access — uses cache only):
|
||||
|
||||
python3 .github/scripts/ci/generate_submissions.py --offline
|
||||
|
||||
Run in CI:
|
||||
|
||||
python3 .github/scripts/ci/generate_submissions.py \\
|
||||
--owner shiaho777 --repo drukscode \\
|
||||
--maintainers shiaho777 \\
|
||||
--token "$GITHUB_TOKEN"
|
||||
|
||||
Exit codes:
|
||||
0 = file was generated successfully (may have committed nothing if
|
||||
nothing changed)
|
||||
1 = a fatal error occurred
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
MODULES_DIR = REPO_ROOT / "modules"
|
||||
SUBMISSIONS_PATH = MODULES_DIR / "submissions.json"
|
||||
REGISTRY_PATH = MODULES_DIR / "registry.json"
|
||||
|
||||
|
||||
# ───────────────────────── git helpers ─────────────────────────────────
|
||||
|
||||
def _git(*args: str) -> str:
|
||||
"""Run a git command relative to the repo root and return stdout."""
|
||||
result = subprocess.run(
|
||||
["git", *args],
|
||||
cwd=str(REPO_ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
|
||||
def _last_commit_for(path: Path) -> str | None:
|
||||
"""Return the SHA of the most recent commit that touched `path`."""
|
||||
try:
|
||||
out = _git("log", "-n", "1", "--format=%H", "--", str(path.relative_to(REPO_ROOT)))
|
||||
sha = out.strip()
|
||||
return sha or None
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
|
||||
|
||||
def _commit_metadata(sha: str) -> dict[str, str]:
|
||||
"""
|
||||
Fetch author name, email, ISO timestamp, and subject for a commit.
|
||||
Used as a fallback when the GitHub API can't tell us about a PR.
|
||||
"""
|
||||
out = _git("show", "-s", "--format=%an%n%ae%n%aI%n%s", sha)
|
||||
parts = out.splitlines()
|
||||
return {
|
||||
"author_name": parts[0] if len(parts) > 0 else "",
|
||||
"author_email": parts[1] if len(parts) > 1 else "",
|
||||
"author_date": parts[2] if len(parts) > 2 else "",
|
||||
"subject": parts[3] if len(parts) > 3 else "",
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
_PR_REF_RE = re.compile(r"(?:\(|\s)#(\d+)(?:\)|\b)")
|
||||
|
||||
|
||||
def _pr_numbers_from_text(*texts: str) -> list[int]:
|
||||
"""Extract GitHub PR numbers mentioned in commit subjects / bodies."""
|
||||
found: list[int] = []
|
||||
seen: set[int] = set()
|
||||
for text in texts:
|
||||
if not text:
|
||||
continue
|
||||
for match in _PR_REF_RE.finditer(text):
|
||||
num = int(match.group(1))
|
||||
if num not in seen:
|
||||
seen.add(num)
|
||||
found.append(num)
|
||||
return found
|
||||
|
||||
|
||||
def _all_commits_for(path: Path) -> list[str]:
|
||||
"""Return every commit SHA that touched `path`, newest first."""
|
||||
try:
|
||||
out = _git("log", "--format=%H", "--", str(path.relative_to(REPO_ROOT)))
|
||||
return [line.strip() for line in out.splitlines() if line.strip()]
|
||||
except subprocess.CalledProcessError:
|
||||
return []
|
||||
|
||||
|
||||
# ───────────────────────── GitHub API helpers ──────────────────────────
|
||||
|
||||
@dataclass
|
||||
class GitHubClient:
|
||||
"""Tiny GitHub REST client that uses urllib so we stay stdlib-only."""
|
||||
|
||||
owner: str
|
||||
repo: str
|
||||
token: str | None = None
|
||||
timeout: int = 15
|
||||
|
||||
def _request(self, url: str) -> Any:
|
||||
headers = {
|
||||
"Accept": "application/vnd.github+json",
|
||||
"User-Agent": "webtoapp-submissions-generator",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
}
|
||||
if self.token:
|
||||
headers["Authorization"] = f"Bearer {self.token}"
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
def pulls_for_commit(self, sha: str) -> list[dict[str, Any]]:
|
||||
"""Return PRs associated with the given commit SHA."""
|
||||
url = (
|
||||
f"https://api.github.com/repos/{self.owner}/{self.repo}"
|
||||
f"/commits/{sha}/pulls"
|
||||
)
|
||||
try:
|
||||
return self._request(url) or []
|
||||
except urllib.error.HTTPError as e:
|
||||
# 404 just means no PR was associated with this commit, which
|
||||
# is the normal case for a maintainer direct-push. Anything
|
||||
# else is worth a note in the log.
|
||||
if e.code == 404:
|
||||
return []
|
||||
print(f"⚠️ GitHub API error for commit {sha[:7]}: {e}", file=sys.stderr)
|
||||
return []
|
||||
except urllib.error.URLError as e:
|
||||
print(f"⚠️ GitHub API URLError for commit {sha[:7]}: {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
def user(self, login: str) -> dict[str, Any] | None:
|
||||
"""Resolve a GitHub login to its display name / avatar URL."""
|
||||
url = f"https://api.github.com/users/{urllib.parse.quote(login)}"
|
||||
try:
|
||||
return self._request(url)
|
||||
except urllib.error.HTTPError:
|
||||
return None
|
||||
except urllib.error.URLError:
|
||||
return None
|
||||
|
||||
def commit_author_login(self, sha: str) -> str:
|
||||
"""Resolve the GitHub login of a commit's author via the commit API."""
|
||||
url = f"https://api.github.com/repos/{self.owner}/{self.repo}/commits/{sha}"
|
||||
try:
|
||||
commit_info = self._request(url)
|
||||
author = commit_info.get("author") or {}
|
||||
return author.get("login") or ""
|
||||
except (urllib.error.HTTPError, urllib.error.URLError):
|
||||
return ""
|
||||
|
||||
def get_pull(self, number: int) -> dict[str, Any] | None:
|
||||
"""Fetch a single pull request by number."""
|
||||
url = (
|
||||
f"https://api.github.com/repos/{self.owner}/{self.repo}"
|
||||
f"/pulls/{int(number)}"
|
||||
)
|
||||
try:
|
||||
return self._request(url)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code != 404:
|
||||
print(f"⚠️ GitHub API error for PR #{number}: {e}", file=sys.stderr)
|
||||
return None
|
||||
except urllib.error.URLError as e:
|
||||
print(f"⚠️ GitHub API URLError for PR #{number}: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
# ───────────────────────── core logic ──────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class Contributor:
|
||||
"""A secondary contributor to a module (everyone except the original submitter)."""
|
||||
|
||||
login: str = ""
|
||||
name: str = ""
|
||||
avatar: str = ""
|
||||
profile: str = ""
|
||||
|
||||
def to_json(self) -> dict[str, str]:
|
||||
out: dict[str, str] = {}
|
||||
if self.login:
|
||||
out["login"] = self.login
|
||||
if self.name:
|
||||
out["name"] = self.name
|
||||
if self.avatar:
|
||||
out["avatarUrl"] = self.avatar
|
||||
if self.profile:
|
||||
out["profileUrl"] = self.profile
|
||||
return out
|
||||
|
||||
|
||||
@dataclass
|
||||
class Submission:
|
||||
"""In-memory representation of a single `submissions.json` entry."""
|
||||
|
||||
pr_number: int | None = None
|
||||
pr_url: str | None = None
|
||||
submitted_at: str | None = None
|
||||
direct: bool = False
|
||||
submitter_login: str = ""
|
||||
submitter_name: str = ""
|
||||
submitter_avatar: str = ""
|
||||
submitter_profile: str = ""
|
||||
contributors: list[Contributor] = field(default_factory=list)
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
out: dict[str, Any] = {}
|
||||
if self.pr_number is not None:
|
||||
out["prNumber"] = self.pr_number
|
||||
if self.pr_url:
|
||||
out["prUrl"] = self.pr_url
|
||||
if self.submitted_at:
|
||||
out["submittedAt"] = self.submitted_at
|
||||
if self.direct:
|
||||
out["direct"] = True
|
||||
submitter: dict[str, str] = {}
|
||||
if self.submitter_login:
|
||||
submitter["login"] = self.submitter_login
|
||||
if self.submitter_name:
|
||||
submitter["name"] = self.submitter_name
|
||||
if self.submitter_avatar:
|
||||
submitter["avatarUrl"] = self.submitter_avatar
|
||||
if self.submitter_profile:
|
||||
submitter["profileUrl"] = self.submitter_profile
|
||||
if submitter:
|
||||
out["submitter"] = submitter
|
||||
if self.contributors:
|
||||
out["contributors"] = [c.to_json() for c in self.contributors if c.login]
|
||||
return out
|
||||
|
||||
|
||||
def _read_existing_submissions() -> dict[str, Any]:
|
||||
"""Read the previous run's output so unchanged modules don't re-fetch."""
|
||||
if not SUBMISSIONS_PATH.is_file():
|
||||
return {}
|
||||
try:
|
||||
data = json.loads(SUBMISSIONS_PATH.read_text(encoding="utf-8"))
|
||||
return data.get("submissions") or {}
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return {}
|
||||
|
||||
|
||||
def _read_registry() -> list[dict[str, Any]]:
|
||||
"""Read the registry to know which `id` ↔ `path` pairs to attribute."""
|
||||
if not REGISTRY_PATH.is_file():
|
||||
return []
|
||||
try:
|
||||
data = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
||||
modules = data.get("modules")
|
||||
if isinstance(modules, list):
|
||||
return [m for m in modules if isinstance(m, dict)]
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
return []
|
||||
|
||||
|
||||
def _resolve_for_path(
|
||||
module_path: Path,
|
||||
client: GitHubClient | None,
|
||||
maintainers: set[str],
|
||||
cache: dict[str, Any],
|
||||
) -> Submission | None:
|
||||
"""Build a Submission for the module folder at `module_path`."""
|
||||
|
||||
sha = _last_commit_for(module_path)
|
||||
if sha is None:
|
||||
return None
|
||||
|
||||
# We attribute to the *introducing* commit when possible — that's the
|
||||
# one that landed the module on main. The simplest proxy in `git log`
|
||||
# is the oldest commit that touched the folder.
|
||||
introducing_sha: str | None = None
|
||||
try:
|
||||
out = _git(
|
||||
"log",
|
||||
"--diff-filter=A",
|
||||
"--format=%H",
|
||||
"--",
|
||||
str(module_path.relative_to(REPO_ROOT) / "module.json"),
|
||||
)
|
||||
intro_lines = [line.strip() for line in out.splitlines() if line.strip()]
|
||||
if intro_lines:
|
||||
introducing_sha = intro_lines[-1]
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
target_sha = introducing_sha or sha
|
||||
|
||||
# In offline mode, fall back to git metadata only.
|
||||
if client is None:
|
||||
meta = _commit_metadata(target_sha)
|
||||
# We still gate maintainer direct-pushes by the configured allowlist,
|
||||
# so a stranger committing directly without a PR isn't silently
|
||||
# promoted into the catalog.
|
||||
login = "" # unknown without GitHub API
|
||||
if not login and not maintainers:
|
||||
return None
|
||||
return Submission(
|
||||
direct=True,
|
||||
submitted_at=meta["author_date"],
|
||||
submitter_login=meta["author_name"],
|
||||
submitter_name=meta["author_name"],
|
||||
)
|
||||
|
||||
meta = _commit_metadata(target_sha)
|
||||
pulls = client.pulls_for_commit(target_sha)
|
||||
|
||||
# Squash / rebase merges often leave commits/{sha}/pulls empty even though
|
||||
# the subject carries "(#123)". Fall back to parsing PR refs from git
|
||||
# metadata and fetching those PRs directly.
|
||||
if not pulls:
|
||||
for pr_number in _pr_numbers_from_text(meta.get("subject", "")):
|
||||
pr_payload = client.get_pull(pr_number)
|
||||
if pr_payload:
|
||||
pulls.append(pr_payload)
|
||||
|
||||
merged_pulls = [p for p in pulls if p.get("merged_at")]
|
||||
merged_on_main = [
|
||||
p for p in merged_pulls
|
||||
if (p.get("base") or {}).get("ref", "main") == "main"
|
||||
]
|
||||
if merged_on_main:
|
||||
merged_pulls = merged_on_main
|
||||
|
||||
if merged_pulls:
|
||||
# Prefer the earliest-merged PR — that is the one that introduced
|
||||
# the module to main. Subsequent updates also produce PRs but they
|
||||
# are version bumps, not the original submission.
|
||||
pr = sorted(merged_pulls, key=lambda p: p.get("merged_at", ""))[0]
|
||||
author = pr.get("user") or {}
|
||||
login = author.get("login") or ""
|
||||
avatar = author.get("avatar_url") or ""
|
||||
profile = author.get("html_url") or (
|
||||
f"https://github.com/{login}" if login else ""
|
||||
)
|
||||
|
||||
# The PR payload doesn't always carry the user's display name; fetch
|
||||
# it lazily and cache so we don't repeat the call for the same login.
|
||||
display_name = ""
|
||||
if login:
|
||||
cached = cache.setdefault("users", {}).get(login)
|
||||
if cached is None:
|
||||
resolved = client.user(login)
|
||||
cached = {
|
||||
"name": (resolved or {}).get("name") or login,
|
||||
"avatar": (resolved or {}).get("avatar_url") or avatar,
|
||||
}
|
||||
cache["users"][login] = cached
|
||||
display_name = cached.get("name") or login
|
||||
avatar = cached.get("avatar") or avatar
|
||||
|
||||
return Submission(
|
||||
pr_number=pr.get("number"),
|
||||
pr_url=pr.get("html_url"),
|
||||
submitted_at=pr.get("merged_at"),
|
||||
direct=False,
|
||||
submitter_login=login,
|
||||
submitter_name=display_name,
|
||||
submitter_avatar=avatar,
|
||||
submitter_profile=profile,
|
||||
)
|
||||
|
||||
# No merged PR was found. This is a direct push to main. The main branch
|
||||
# is branch-protected, so a direct push has already been authorised by a
|
||||
# maintainer (or admin) — we trust that gate rather than re-checking the
|
||||
# GitHub login, which is unreliable for commits whose author email isn't
|
||||
# linked to a GitHub account (the API returns `author: null` and we'd
|
||||
# silently hide legitimate maintainer pushes).
|
||||
meta = _commit_metadata(target_sha)
|
||||
# Try to resolve the GitHub login from the commit's email when possible.
|
||||
# `git log` doesn't carry the GitHub login, so we look it up by querying
|
||||
# the commit endpoint, which embeds the GH author when GitHub knows them.
|
||||
login = ""
|
||||
try:
|
||||
url = f"https://api.github.com/repos/{client.owner}/{client.repo}/commits/{target_sha}"
|
||||
commit_info = client._request(url)
|
||||
author = commit_info.get("author") or {}
|
||||
login = author.get("login") or ""
|
||||
except (urllib.error.HTTPError, urllib.error.URLError):
|
||||
pass
|
||||
|
||||
if not login:
|
||||
# Commit email isn't linked to a GitHub account, so the API can't
|
||||
# tell us who authored it. The push already cleared branch
|
||||
# protection, so attribute it to the repo owner as a fallback.
|
||||
login = client.owner
|
||||
|
||||
# Prefer PR attribution above. For true direct pushes we still record
|
||||
# any resolved GitHub author — modules only land on main after review
|
||||
# or maintainer push, so hiding non-maintainer authors made real
|
||||
# contributor modules disappear from the in-app market.
|
||||
if (
|
||||
maintainers
|
||||
and login
|
||||
and login.lower() not in {m.lower() for m in maintainers}
|
||||
and login.lower() in {"github-actions[bot]", "web-flow", "dependabot[bot]"}
|
||||
):
|
||||
return None
|
||||
|
||||
avatar = ""
|
||||
profile = ""
|
||||
display_name = login or meta["author_name"]
|
||||
if login:
|
||||
cached = cache.setdefault("users", {}).get(login)
|
||||
if cached is None:
|
||||
resolved = client.user(login)
|
||||
cached = {
|
||||
"name": (resolved or {}).get("name") or login,
|
||||
"avatar": (resolved or {}).get("avatar_url") or "",
|
||||
"profile": (resolved or {}).get("html_url") or f"https://github.com/{login}",
|
||||
}
|
||||
cache["users"][login] = cached
|
||||
display_name = cached.get("name") or login
|
||||
avatar = cached.get("avatar") or ""
|
||||
profile = cached.get("profile") or f"https://github.com/{login}"
|
||||
|
||||
return Submission(
|
||||
direct=True,
|
||||
submitted_at=meta["author_date"],
|
||||
submitter_login=login,
|
||||
submitter_name=display_name,
|
||||
submitter_avatar=avatar,
|
||||
submitter_profile=profile,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_contributors(
|
||||
module_path: Path,
|
||||
submitter_login: str,
|
||||
client: GitHubClient | None,
|
||||
cache: dict[str, Any],
|
||||
) -> list[Contributor]:
|
||||
"""
|
||||
Collect secondary contributors for a module folder.
|
||||
|
||||
Walks every commit that touched the folder, resolves each distinct
|
||||
author's GitHub login via the commit API, and returns everyone except
|
||||
the original submitter (and bots/github-actions). Offline mode and
|
||||
unresolvable logins yield an empty list — contributors are a
|
||||
best-effort enrichment, never a hard requirement.
|
||||
"""
|
||||
if client is None:
|
||||
return []
|
||||
|
||||
shas = _all_commits_for(module_path)
|
||||
seen_logins: set[str] = set()
|
||||
if submitter_login:
|
||||
seen_logins.add(submitter_login.lower())
|
||||
seen_logins.add("github-actions[bot]")
|
||||
seen_logins.add("web-flow")
|
||||
|
||||
contributors: list[Contributor] = []
|
||||
users = cache.setdefault("users", {})
|
||||
for sha in shas:
|
||||
login = client.commit_author_login(sha)
|
||||
if not login or login.lower() in seen_logins:
|
||||
continue
|
||||
seen_logins.add(login.lower())
|
||||
|
||||
cached = users.get(login)
|
||||
if cached is None:
|
||||
resolved = client.user(login)
|
||||
cached = {
|
||||
"name": (resolved or {}).get("name") or login,
|
||||
"avatar": (resolved or {}).get("avatar_url") or "",
|
||||
"profile": (resolved or {}).get("html_url") or f"https://github.com/{login}",
|
||||
}
|
||||
users[login] = cached
|
||||
|
||||
contributors.append(Contributor(
|
||||
login=login,
|
||||
name=cached.get("name") or login,
|
||||
avatar=cached.get("avatar") or "",
|
||||
profile=cached.get("profile") or f"https://github.com/{login}",
|
||||
))
|
||||
|
||||
return contributors
|
||||
|
||||
|
||||
# ───────────────────────── CLI entry ───────────────────────────────────
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0])
|
||||
parser.add_argument("--owner", default=os.environ.get("REPO_OWNER", "shiaho777"))
|
||||
parser.add_argument("--repo", default=os.environ.get("REPO_NAME", "drukscode"))
|
||||
parser.add_argument("--token", default=os.environ.get("GITHUB_TOKEN"))
|
||||
parser.add_argument(
|
||||
"--maintainers",
|
||||
default=os.environ.get("MAINTAINERS", ""),
|
||||
help=(
|
||||
"Comma-separated GitHub logins allowed to seed modules via "
|
||||
"direct push (no PR). Anything else without a merged PR is "
|
||||
"deliberately hidden from the catalog."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--offline",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Skip the GitHub API entirely; useful for smoke-testing the "
|
||||
"script locally. Falls back to git metadata only."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Generate to a temp file and exit with code 1 if the result "
|
||||
"differs from the committed file. Used as a guard rail in CI "
|
||||
"for PRs that touch modules/."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not MODULES_DIR.is_dir():
|
||||
print(f"❌ {MODULES_DIR} does not exist", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
maintainers: set[str] = {
|
||||
m.strip() for m in args.maintainers.split(",") if m.strip()
|
||||
}
|
||||
|
||||
client: GitHubClient | None
|
||||
if args.offline:
|
||||
client = None
|
||||
else:
|
||||
client = GitHubClient(owner=args.owner, repo=args.repo, token=args.token)
|
||||
|
||||
registry = _read_registry()
|
||||
# Map registry path → id so we attribute each folder to its catalog id.
|
||||
path_to_id: dict[str, str] = {}
|
||||
for entry in registry:
|
||||
p = entry.get("path")
|
||||
i = entry.get("id")
|
||||
if isinstance(p, str) and isinstance(i, str):
|
||||
path_to_id[p] = i
|
||||
|
||||
# Walk every kebab-case folder; the validator already ensures the layout.
|
||||
folders = sorted(
|
||||
p for p in MODULES_DIR.iterdir()
|
||||
if p.is_dir() and not p.name.startswith(".")
|
||||
)
|
||||
|
||||
cache: dict[str, Any] = {}
|
||||
submissions: dict[str, Any] = {}
|
||||
skipped: list[str] = []
|
||||
for folder in folders:
|
||||
module_id = path_to_id.get(folder.name)
|
||||
if not module_id:
|
||||
# Folder without a registry entry — the validator will already
|
||||
# be screaming about this; we just skip it.
|
||||
skipped.append(folder.name)
|
||||
continue
|
||||
submission = _resolve_for_path(folder, client, maintainers, cache)
|
||||
if submission is None:
|
||||
# Hidden by design: no merged PR and not a maintainer direct
|
||||
# push. Surface this in the log so reviewers can tell what's
|
||||
# going on.
|
||||
skipped.append(folder.name)
|
||||
continue
|
||||
submission.contributors = _resolve_contributors(
|
||||
folder, submission.submitter_login, client, cache
|
||||
)
|
||||
submissions[module_id] = submission.to_json()
|
||||
|
||||
output = {
|
||||
"schema": 1,
|
||||
"submissions": submissions,
|
||||
}
|
||||
|
||||
if not args.check:
|
||||
existing_text = SUBMISSIONS_PATH.read_text(encoding="utf-8") if SUBMISSIONS_PATH.is_file() else ""
|
||||
try:
|
||||
existing_obj = json.loads(existing_text) if existing_text else {}
|
||||
except json.JSONDecodeError:
|
||||
existing_obj = {}
|
||||
existing_subs = existing_obj.get("submissions") or {}
|
||||
if existing_subs == submissions and "generatedAt" in existing_obj:
|
||||
output["generatedAt"] = existing_obj["generatedAt"]
|
||||
else:
|
||||
output["generatedAt"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
else:
|
||||
output["generatedAt"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
ordered = {
|
||||
"schema": output["schema"],
|
||||
"generatedAt": output["generatedAt"],
|
||||
"submissions": output["submissions"],
|
||||
}
|
||||
serialised = json.dumps(ordered, indent=2, ensure_ascii=False) + "\n"
|
||||
|
||||
if args.check:
|
||||
# Diff against the committed file. We tolerate a missing file by
|
||||
# treating it as empty. `generatedAt` is excluded from the diff
|
||||
# so check mode is deterministic.
|
||||
existing_text = SUBMISSIONS_PATH.read_text(encoding="utf-8") if SUBMISSIONS_PATH.is_file() else ""
|
||||
try:
|
||||
existing_obj = json.loads(existing_text) if existing_text else {}
|
||||
except json.JSONDecodeError:
|
||||
existing_obj = {}
|
||||
|
||||
existing_subs = existing_obj.get("submissions") or {}
|
||||
if existing_subs == submissions:
|
||||
print(f"✅ submissions.json is up to date ({len(submissions)} entries)")
|
||||
if skipped:
|
||||
print(f" ({len(skipped)} folder(s) hidden from catalog: {', '.join(skipped)})")
|
||||
return 0
|
||||
print("❌ submissions.json is stale — re-run generate_submissions.py and commit the result.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
SUBMISSIONS_PATH.write_text(serialised, encoding="utf-8")
|
||||
print(f"✅ Wrote {SUBMISSIONS_PATH.relative_to(REPO_ROOT)} with {len(submissions)} entries")
|
||||
if skipped:
|
||||
print(f" ({len(skipped)} folder(s) hidden from catalog: {', '.join(skipped)})")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
671
.github/scripts/ci/validate_modules.py
vendored
Executable file
671
.github/scripts/ci/validate_modules.py
vendored
Executable file
@ -0,0 +1,671 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Validate the DruksCode Module Market catalog at `modules/`.
|
||||
|
||||
This is the source of truth for what `modules/README.md` claims gets
|
||||
checked at CI time. Every rule here corresponds to a real failure mode
|
||||
that would either reject a module at install time or quietly waste space
|
||||
on disk.
|
||||
|
||||
The validator is intentionally written with only Python's standard
|
||||
library — no pip install step, no extra container — so the CI job stays
|
||||
fast and deterministic.
|
||||
|
||||
Run locally:
|
||||
|
||||
python3 .github/scripts/ci/validate_modules.py
|
||||
|
||||
Exit codes:
|
||||
0 = catalog is valid
|
||||
1 = at least one error was found (CI fails)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
|
||||
# ───────────────────────── enum values (must mirror the Kotlin source) ─
|
||||
|
||||
# `ModuleCategory` enum in `core/extension/ExtensionModule.kt`.
|
||||
ALLOWED_CATEGORIES: set[str] = {
|
||||
"CONTENT_FILTER", "CONTENT_ENHANCE", "STYLE_MODIFIER", "THEME",
|
||||
"FUNCTION_ENHANCE", "AUTOMATION", "NAVIGATION", "DATA_EXTRACT",
|
||||
"DATA_SAVE", "INTERACTION", "ACCESSIBILITY", "MEDIA", "VIDEO",
|
||||
"IMAGE", "AUDIO", "SECURITY", "ANTI_TRACKING", "SOCIAL", "SHOPPING",
|
||||
"READING", "TRANSLATE", "DEVELOPER", "OTHER",
|
||||
}
|
||||
|
||||
# `ModuleRunTime` enum.
|
||||
ALLOWED_RUN_AT: set[str] = {
|
||||
"DOCUMENT_START", "DOCUMENT_END", "DOCUMENT_IDLE",
|
||||
"CONTEXT_MENU", "BEFORE_UNLOAD",
|
||||
}
|
||||
|
||||
# `ModulePermission` enum.
|
||||
ALLOWED_PERMISSIONS: set[str] = {
|
||||
"DOM_ACCESS", "DOM_OBSERVE", "CSS_INJECT", "STORAGE", "COOKIE",
|
||||
"INDEXED_DB", "CACHE", "NETWORK", "WEBSOCKET", "FETCH_INTERCEPT",
|
||||
"CLIPBOARD", "NOTIFICATION", "ALERT", "KEYBOARD", "MOUSE", "TOUCH",
|
||||
"LOCATION", "CAMERA", "MICROPHONE", "DEVICE_INFO", "MEDIA",
|
||||
"FULLSCREEN", "PICTURE_IN_PICTURE", "SCREEN_CAPTURE", "DOWNLOAD",
|
||||
"FILE_ACCESS", "EVAL", "IFRAME", "WINDOW_OPEN", "HISTORY",
|
||||
"NAVIGATION",
|
||||
}
|
||||
|
||||
# `ConfigItemType` enum.
|
||||
ALLOWED_CONFIG_TYPES: set[str] = {
|
||||
"TEXT", "TEXTAREA", "NUMBER", "BOOLEAN", "SELECT", "MULTI_SELECT",
|
||||
"RADIO", "CHECKBOX", "COLOR", "URL", "EMAIL", "PASSWORD", "REGEX",
|
||||
"CSS_SELECTOR", "JAVASCRIPT", "JSON", "RANGE", "DATE", "TIME",
|
||||
"DATETIME", "FILE", "IMAGE",
|
||||
}
|
||||
|
||||
ALLOWED_SOURCE_TYPES: set[str] = {"CUSTOM", "CHROME_EXTENSION"}
|
||||
STORE_ID_RE = re.compile(r"^[a-p]{32}$")
|
||||
|
||||
|
||||
# ───────────────────────── regex helpers ───────────────────────────────
|
||||
|
||||
KEBAB_CASE_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
||||
SEMVER_RE = re.compile(r"^\d+(?:\.\d+){0,3}(?:[-+][\w.-]+)?$")
|
||||
|
||||
# Files we let people drop in besides the three the runtime actually
|
||||
# downloads (module.json, main.js, style.css).
|
||||
ALLOWED_EXTRA_FILES: set[str] = {
|
||||
"README.md", # nice for module pages on GitHub
|
||||
"CHANGELOG.md",
|
||||
"LICENSE",
|
||||
"LICENSE.md",
|
||||
".gitkeep",
|
||||
}
|
||||
|
||||
# Image files contributors may drop next to `main.js` for use as the
|
||||
# module icon. The CI generator does not download these — they are served
|
||||
# via the same GitHub raw / jsDelivr fallback the runtime already uses.
|
||||
ALLOWED_ICON_FILES: set[str] = {
|
||||
"icon.png", "icon.svg", "icon.webp", "icon.jpg", "icon.jpeg",
|
||||
}
|
||||
|
||||
# Maximum size for an inlined icon. Larger icons should be hosted off-repo
|
||||
# and referenced via an absolute `iconUrl`. 256 KB is a forgiving limit:
|
||||
# a 256x256 PNG with reasonable compression sits comfortably below it.
|
||||
MAX_ICON_BYTES = 256 * 1024
|
||||
|
||||
|
||||
@dataclass
|
||||
class Report:
|
||||
"""Accumulates diagnostics and pretty-prints them at the end."""
|
||||
|
||||
errors: list[str] = field(default_factory=list)
|
||||
warnings: list[str] = field(default_factory=list)
|
||||
|
||||
def error(self, where: str, message: str) -> None:
|
||||
self.errors.append(f" ❌ {where}: {message}")
|
||||
|
||||
def warning(self, where: str, message: str) -> None:
|
||||
self.warnings.append(f" ⚠️ {where}: {message}")
|
||||
|
||||
def ok(self) -> bool:
|
||||
return not self.errors
|
||||
|
||||
def render(self) -> str:
|
||||
lines: list[str] = []
|
||||
if self.errors:
|
||||
lines.append(f"\n{len(self.errors)} error(s):")
|
||||
lines.extend(self.errors)
|
||||
if self.warnings:
|
||||
lines.append(f"\n{len(self.warnings)} warning(s):")
|
||||
lines.extend(self.warnings)
|
||||
if not lines:
|
||||
lines.append("\nAll module checks passed.")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ───────────────────────── primitive checks ────────────────────────────
|
||||
|
||||
def _is_str(value: Any) -> bool:
|
||||
return isinstance(value, str)
|
||||
|
||||
|
||||
def _expect_str(report: Report, where: str, value: Any, field: str) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if not _is_str(value):
|
||||
report.error(where, f"`{field}` must be a string, got {type(value).__name__}")
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def _expect_list_of_str(report: Report, where: str, value: Any, field: str) -> list[str]:
|
||||
if value is None:
|
||||
return []
|
||||
if not isinstance(value, list) or any(not _is_str(v) for v in value):
|
||||
report.error(where, f"`{field}` must be a list of strings")
|
||||
return []
|
||||
return value
|
||||
|
||||
|
||||
def _validate_url_matches(
|
||||
report: Report, where: str, url_matches: Any
|
||||
) -> None:
|
||||
if url_matches is None:
|
||||
return
|
||||
if not isinstance(url_matches, list):
|
||||
report.error(where, "`urlMatches` must be a list")
|
||||
return
|
||||
for i, rule in enumerate(url_matches):
|
||||
rule_loc = f"{where}::urlMatches[{i}]"
|
||||
if not isinstance(rule, dict):
|
||||
report.error(rule_loc, "must be an object with at least `pattern`")
|
||||
continue
|
||||
pattern = rule.get("pattern")
|
||||
if not _is_str(pattern) or not pattern:
|
||||
report.error(rule_loc, "`pattern` is required and must be a non-empty string")
|
||||
for flag in ("isRegex", "exclude"):
|
||||
if flag in rule and not isinstance(rule[flag], bool):
|
||||
report.error(rule_loc, f"`{flag}` must be a boolean")
|
||||
|
||||
|
||||
def _validate_author(report: Report, where: str, author: Any) -> None:
|
||||
if author is None:
|
||||
return
|
||||
if not isinstance(author, dict):
|
||||
report.error(where, "`author` must be an object")
|
||||
return
|
||||
name = author.get("name")
|
||||
if not _is_str(name) or not name.strip():
|
||||
report.error(where, "`author.name` is required")
|
||||
for opt_key in ("email", "url", "qq"):
|
||||
if opt_key in author and author[opt_key] is not None and not _is_str(author[opt_key]):
|
||||
report.error(where, f"`author.{opt_key}` must be a string")
|
||||
|
||||
|
||||
# ───────────────────────── manifest checks ─────────────────────────────
|
||||
|
||||
def _validate_module_json(
|
||||
report: Report, folder: Path, manifest: dict[str, Any]
|
||||
) -> None:
|
||||
"""Validate a per-module `module.json`."""
|
||||
|
||||
where = f"modules/{folder.name}/module.json"
|
||||
|
||||
# Required fields.
|
||||
if not _is_str(manifest.get("id")) or not manifest["id"].strip():
|
||||
report.error(where, "`id` is required and must be a non-empty string")
|
||||
if not _is_str(manifest.get("name")) or not manifest["name"].strip():
|
||||
report.error(where, "`name` is required and must be a non-empty string")
|
||||
|
||||
# Enums (with a friendly hint when wrong).
|
||||
category = manifest.get("category", "OTHER")
|
||||
if not _is_str(category) or category not in ALLOWED_CATEGORIES:
|
||||
report.error(
|
||||
where,
|
||||
f"`category` must be one of {sorted(ALLOWED_CATEGORIES)}, got {category!r}",
|
||||
)
|
||||
|
||||
run_at = manifest.get("runAt", "DOCUMENT_END")
|
||||
if not _is_str(run_at) or run_at not in ALLOWED_RUN_AT:
|
||||
report.error(
|
||||
where,
|
||||
f"`runAt` must be one of {sorted(ALLOWED_RUN_AT)}, got {run_at!r}",
|
||||
)
|
||||
|
||||
# Version (object form).
|
||||
version = manifest.get("version")
|
||||
if version is None:
|
||||
report.error(where, "`version` is required (object with `code`, `name`, `changelog`)")
|
||||
elif not isinstance(version, dict):
|
||||
report.error(where, "`version` must be an object — see modules/README.md schema")
|
||||
else:
|
||||
v_code = version.get("code")
|
||||
if not isinstance(v_code, int) or v_code < 1:
|
||||
report.error(where, "`version.code` must be a positive integer")
|
||||
v_name = version.get("name")
|
||||
if not _is_str(v_name) or not SEMVER_RE.match(v_name or ""):
|
||||
report.error(where, f"`version.name` must be semver-shaped, got {v_name!r}")
|
||||
if "changelog" in version and version["changelog"] is not None and not _is_str(version["changelog"]):
|
||||
report.error(where, "`version.changelog` must be a string")
|
||||
|
||||
# Permissions.
|
||||
for perm in _expect_list_of_str(report, where, manifest.get("permissions"), "permissions"):
|
||||
if perm not in ALLOWED_PERMISSIONS:
|
||||
report.error(where, f"unknown permission {perm!r}")
|
||||
|
||||
# urlMatches and author.
|
||||
_validate_url_matches(report, where, manifest.get("urlMatches"))
|
||||
_validate_author(report, where, manifest.get("author"))
|
||||
|
||||
# configItems.
|
||||
config_items = manifest.get("configItems", [])
|
||||
if config_items is None:
|
||||
config_items = []
|
||||
if not isinstance(config_items, list):
|
||||
report.error(where, "`configItems` must be a list")
|
||||
return
|
||||
seen_keys: set[str] = set()
|
||||
for i, item in enumerate(config_items):
|
||||
item_loc = f"{where}::configItems[{i}]"
|
||||
if not isinstance(item, dict):
|
||||
report.error(item_loc, "must be an object")
|
||||
continue
|
||||
key = item.get("key")
|
||||
if not _is_str(key) or not key.strip():
|
||||
report.error(item_loc, "`key` is required")
|
||||
elif key in seen_keys:
|
||||
report.error(item_loc, f"duplicate key {key!r}")
|
||||
else:
|
||||
seen_keys.add(key)
|
||||
|
||||
if not _is_str(item.get("name")) or not item["name"].strip():
|
||||
report.error(item_loc, "`name` is required")
|
||||
|
||||
type_ = item.get("type", "TEXT")
|
||||
if not _is_str(type_) or type_ not in ALLOWED_CONFIG_TYPES:
|
||||
report.error(item_loc, f"`type` must be one of {sorted(ALLOWED_CONFIG_TYPES)}")
|
||||
|
||||
if "required" in item and not isinstance(item["required"], bool):
|
||||
report.error(item_loc, "`required` must be a boolean")
|
||||
|
||||
for str_field in ("description", "defaultValue", "placeholder", "validation"):
|
||||
if str_field in item and item[str_field] is not None and not _is_str(item[str_field]):
|
||||
report.error(item_loc, f"`{str_field}` must be a string")
|
||||
|
||||
# SELECT / MULTI_SELECT / RADIO need `options`.
|
||||
if type_ in {"SELECT", "MULTI_SELECT", "RADIO"}:
|
||||
options = item.get("options")
|
||||
if not isinstance(options, list) or not options:
|
||||
report.error(item_loc, f"`options` is required for type {type_}")
|
||||
elif not all(_is_str(o) for o in options):
|
||||
report.error(item_loc, f"`options` for type {type_} must be a list of strings; the app parses ModuleConfigItem.options as List<String>, so object forms like [{{\"value\",\"label\"}}] fail Gson deserialization")
|
||||
|
||||
|
||||
# ───────────────────────── registry checks ─────────────────────────────
|
||||
|
||||
def _validate_registry_entry(
|
||||
report: Report, entry: dict[str, Any], index: int
|
||||
) -> None:
|
||||
where = f"registry.json::modules[{index}]"
|
||||
|
||||
for required in ("id", "path", "name"):
|
||||
if not _is_str(entry.get(required)) or not entry[required].strip():
|
||||
report.error(where, f"`{required}` is required and must be a non-empty string")
|
||||
|
||||
path = entry.get("path", "")
|
||||
if _is_str(path) and not KEBAB_CASE_RE.match(path):
|
||||
report.error(where, f"`path` must be kebab-case, got {path!r}")
|
||||
|
||||
version = entry.get("version")
|
||||
if not _is_str(version) or not SEMVER_RE.match(version or ""):
|
||||
report.error(where, f"`version` must be a semver string, got {version!r}")
|
||||
|
||||
category = entry.get("category", "OTHER")
|
||||
if not _is_str(category) or category not in ALLOWED_CATEGORIES:
|
||||
report.error(where, f"`category` must be one of the allowed values, got {category!r}")
|
||||
|
||||
run_at = entry.get("runAt", "DOCUMENT_END")
|
||||
if not _is_str(run_at) or run_at not in ALLOWED_RUN_AT:
|
||||
report.error(where, f"`runAt` must be one of the allowed values, got {run_at!r}")
|
||||
|
||||
for perm in _expect_list_of_str(report, where, entry.get("permissions"), "permissions"):
|
||||
if perm not in ALLOWED_PERMISSIONS:
|
||||
report.error(where, f"unknown permission {perm!r}")
|
||||
|
||||
_validate_url_matches(report, where, entry.get("urlMatches"))
|
||||
_validate_author(report, where, entry.get("author"))
|
||||
|
||||
if "minAppVersion" in entry and not isinstance(entry["minAppVersion"], int):
|
||||
report.error(where, "`minAppVersion` must be an integer")
|
||||
|
||||
if "hasCss" in entry and not isinstance(entry["hasCss"], bool):
|
||||
report.error(where, "`hasCss` must be a boolean")
|
||||
|
||||
icon_url = entry.get("iconUrl")
|
||||
if icon_url is not None and not _is_str(icon_url):
|
||||
report.error(where, "`iconUrl` must be a string when present")
|
||||
|
||||
source_type = entry.get("sourceType", "CUSTOM")
|
||||
if not _is_str(source_type) or source_type not in ALLOWED_SOURCE_TYPES:
|
||||
report.error(where, f"`sourceType` must be one of {sorted(ALLOWED_SOURCE_TYPES)}, got {source_type!r}")
|
||||
|
||||
if source_type == "CHROME_EXTENSION":
|
||||
store_id = entry.get("storeId")
|
||||
if not _is_str(store_id) or not STORE_ID_RE.match(store_id or ""):
|
||||
report.error(where, "`storeId` is required for CHROME_EXTENSION and must be a 32-char Chrome Web Store ID")
|
||||
|
||||
|
||||
def _validate_registry(report: Report, registry: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
where = "registry.json"
|
||||
schema = registry.get("schema")
|
||||
if schema != 1:
|
||||
report.error(where, f"`schema` must be 1 (got {schema!r}); future versions need a parser update")
|
||||
if "updatedAt" in registry and not _is_str(registry["updatedAt"]):
|
||||
report.error(where, "`updatedAt` must be a string")
|
||||
|
||||
modules = registry.get("modules")
|
||||
if not isinstance(modules, list):
|
||||
report.error(where, "`modules` must be a list")
|
||||
return []
|
||||
|
||||
seen_ids: set[str] = set()
|
||||
seen_paths: set[str] = set()
|
||||
for i, entry in enumerate(modules):
|
||||
if not isinstance(entry, dict):
|
||||
report.error(f"{where}::modules[{i}]", "must be an object")
|
||||
continue
|
||||
_validate_registry_entry(report, entry, i)
|
||||
|
||||
eid = entry.get("id") if _is_str(entry.get("id")) else None
|
||||
if eid:
|
||||
if eid in seen_ids:
|
||||
report.error(f"{where}::modules[{i}]", f"duplicate id {eid!r}")
|
||||
if not KEBAB_CASE_RE.match(eid):
|
||||
report.error(f"{where}::modules[{i}]", f"id {eid!r} must be kebab-case")
|
||||
seen_ids.add(eid)
|
||||
|
||||
epath = entry.get("path") if _is_str(entry.get("path")) else None
|
||||
if epath:
|
||||
if epath in seen_paths:
|
||||
report.error(f"{where}::modules[{i}]", f"duplicate path {epath!r}")
|
||||
seen_paths.add(epath)
|
||||
|
||||
return [m for m in modules if isinstance(m, dict)]
|
||||
|
||||
|
||||
# ───────────────────────── cross-file consistency ─────────────────────
|
||||
|
||||
def _validate_cross_consistency(
|
||||
report: Report,
|
||||
entry: dict[str, Any],
|
||||
manifest: dict[str, Any],
|
||||
folder: Path,
|
||||
) -> None:
|
||||
"""`module.json` and `registry.json` must agree on the shared fields."""
|
||||
|
||||
where = f"modules/{folder.name}"
|
||||
|
||||
# id
|
||||
if entry.get("id") != manifest.get("id"):
|
||||
report.error(
|
||||
where,
|
||||
f"`id` mismatch: registry says {entry.get('id')!r}, manifest says {manifest.get('id')!r}",
|
||||
)
|
||||
|
||||
# name
|
||||
if entry.get("name") != manifest.get("name"):
|
||||
report.error(
|
||||
where,
|
||||
f"`name` mismatch: registry says {entry.get('name')!r}, manifest says {manifest.get('name')!r}",
|
||||
)
|
||||
|
||||
# version: registry stores the string, manifest stores an object.
|
||||
reg_version = entry.get("version")
|
||||
man_version_obj = manifest.get("version") or {}
|
||||
man_version = man_version_obj.get("name") if isinstance(man_version_obj, dict) else None
|
||||
if reg_version != man_version:
|
||||
report.error(
|
||||
where,
|
||||
f"`version` mismatch: registry={reg_version!r}, module.json::version.name={man_version!r}",
|
||||
)
|
||||
|
||||
# runAt
|
||||
if entry.get("runAt") and manifest.get("runAt") and entry["runAt"] != manifest["runAt"]:
|
||||
report.error(
|
||||
where,
|
||||
f"`runAt` mismatch: registry={entry['runAt']!r}, manifest={manifest['runAt']!r}",
|
||||
)
|
||||
|
||||
# permissions: registry should be a superset (the listing surface) of manifest.
|
||||
reg_perms = set(entry.get("permissions") or [])
|
||||
man_perms = set(manifest.get("permissions") or [])
|
||||
missing_in_registry = man_perms - reg_perms
|
||||
if missing_in_registry:
|
||||
report.error(
|
||||
where,
|
||||
f"manifest declares permissions {sorted(missing_in_registry)} that registry.json does not list",
|
||||
)
|
||||
|
||||
# hasCss must match style.css presence on disk.
|
||||
has_css_flag = bool(entry.get("hasCss"))
|
||||
style_present = (folder / "style.css").is_file()
|
||||
if has_css_flag and not style_present:
|
||||
report.error(
|
||||
where,
|
||||
"registry says `hasCss: true` but no `style.css` file is present",
|
||||
)
|
||||
if style_present and not has_css_flag:
|
||||
report.error(
|
||||
where,
|
||||
"`style.css` exists but registry has `hasCss: false` — it will not be downloaded",
|
||||
)
|
||||
|
||||
|
||||
# ───────────────────────── per-folder structural checks ───────────────
|
||||
|
||||
def _validate_folder_layout(report: Report, folder: Path) -> None:
|
||||
"""Each module folder must look like the schema says it does."""
|
||||
|
||||
where = f"modules/{folder.name}"
|
||||
|
||||
if not KEBAB_CASE_RE.match(folder.name):
|
||||
report.error(where, f"folder name must be kebab-case, got {folder.name!r}")
|
||||
|
||||
if not (folder / "module.json").is_file():
|
||||
report.error(where, "missing required `module.json`")
|
||||
|
||||
if not (folder / "main.js").is_file():
|
||||
report.error(where, "missing required `main.js`")
|
||||
|
||||
# Flag stray files. The runtime ignores them, so they only bloat the repo.
|
||||
expected = {"module.json", "main.js", "style.css"}
|
||||
for child in folder.iterdir():
|
||||
if child.name in expected or child.name in ALLOWED_EXTRA_FILES:
|
||||
continue
|
||||
if child.name in ALLOWED_ICON_FILES:
|
||||
# Icons are size-checked separately further below.
|
||||
continue
|
||||
if child.is_dir():
|
||||
report.warning(
|
||||
where,
|
||||
f"unexpected sub-directory `{child.name}/` — the runtime won't read it",
|
||||
)
|
||||
else:
|
||||
report.warning(
|
||||
where,
|
||||
f"unexpected file `{child.name}` — the runtime won't download it",
|
||||
)
|
||||
|
||||
|
||||
def _validate_icon_coherence(
|
||||
report: Report, folder: Path, entry: dict[str, Any] | None
|
||||
) -> None:
|
||||
"""
|
||||
Cross-check the contributor-supplied icon against the registry.
|
||||
|
||||
The contract:
|
||||
|
||||
- If `iconUrl` is missing/empty, the app falls back to the first letter
|
||||
of the module name. No icon file is needed.
|
||||
- If `iconUrl` looks like a relative path, the corresponding file must
|
||||
actually exist next to `main.js` and stay within the size budget.
|
||||
- If `iconUrl` is absolute (starts with `http://` or `https://`), we
|
||||
defer to the contributor's hosting and do not check the file system.
|
||||
- The file name must come from `ALLOWED_ICON_FILES` so we don't end up
|
||||
shipping random binaries through the catalog.
|
||||
"""
|
||||
where = f"modules/{folder.name}"
|
||||
icon_url = (entry or {}).get("iconUrl")
|
||||
|
||||
icon_files_present = sorted(
|
||||
f for f in ALLOWED_ICON_FILES if (folder / f).is_file()
|
||||
)
|
||||
|
||||
# Warn about extra random icon files when the registry doesn't reference
|
||||
# them — the runtime won't fetch a `logo.png` it has no way to know about.
|
||||
if not icon_url and icon_files_present:
|
||||
report.warning(
|
||||
where,
|
||||
f"icon file(s) {icon_files_present} present but registry has no "
|
||||
"`iconUrl` — they won't be used by the runtime",
|
||||
)
|
||||
|
||||
if not _is_str(icon_url) or not icon_url.strip():
|
||||
return
|
||||
|
||||
icon_url = icon_url.strip()
|
||||
if icon_url.startswith("http://") or icon_url.startswith("https://"):
|
||||
if icon_url.startswith("http://"):
|
||||
report.warning(where, "`iconUrl` uses http://; prefer https for catalog assets")
|
||||
return
|
||||
|
||||
# Relative path case. Reject path traversal and force the file to live
|
||||
# inside the module folder with a known extension.
|
||||
normalised = icon_url.lstrip("./")
|
||||
if normalised.startswith("/") or ".." in normalised.split("/"):
|
||||
report.error(where, f"`iconUrl` must be a path inside the module folder, got {icon_url!r}")
|
||||
return
|
||||
if normalised not in ALLOWED_ICON_FILES:
|
||||
report.error(
|
||||
where,
|
||||
f"`iconUrl` must reference one of {sorted(ALLOWED_ICON_FILES)}, got {icon_url!r}",
|
||||
)
|
||||
return
|
||||
|
||||
icon_path = folder / normalised
|
||||
if not icon_path.is_file():
|
||||
report.error(where, f"`iconUrl` points at {normalised!r} but no such file exists")
|
||||
return
|
||||
|
||||
size = icon_path.stat().st_size
|
||||
if size > MAX_ICON_BYTES:
|
||||
report.error(
|
||||
where,
|
||||
f"`{normalised}` is {size // 1024} KB, over the {MAX_ICON_BYTES // 1024} KB cap; "
|
||||
"trim the icon or host it off-repo via an absolute URL",
|
||||
)
|
||||
|
||||
|
||||
def _validate_main_js(report: Report, folder: Path) -> None:
|
||||
"""Cheap heuristics on `main.js`."""
|
||||
|
||||
main_js = folder / "main.js"
|
||||
if not main_js.is_file():
|
||||
return
|
||||
where = f"modules/{folder.name}/main.js"
|
||||
|
||||
try:
|
||||
content = main_js.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
report.error(where, "must be UTF-8")
|
||||
return
|
||||
|
||||
if not content.strip():
|
||||
report.error(where, "is empty")
|
||||
return
|
||||
|
||||
if len(content) > 512 * 1024:
|
||||
report.warning(where, f"is large ({len(content) // 1024} KB) — consider trimming")
|
||||
|
||||
# No top-level `return` — the IIFE wrapper turns this into a syntax error.
|
||||
for lineno, line in enumerate(content.splitlines(), start=1):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("return ") or stripped == "return;" or stripped == "return":
|
||||
# Could still be inside a function literal at line 1; we can't
|
||||
# parse JS exactly, but this is a strong smell.
|
||||
indent = len(line) - len(line.lstrip())
|
||||
if indent == 0:
|
||||
report.error(
|
||||
where,
|
||||
f"line {lineno}: top-level `return` — the IIFE wrapper would make this a syntax error",
|
||||
)
|
||||
break
|
||||
|
||||
# If `getConfig` is referenced but no configItems are declared in the
|
||||
# manifest, the user will see "undefined" defaults silently.
|
||||
manifest_path = folder / "module.json"
|
||||
if manifest_path.is_file() and "getConfig(" in content:
|
||||
try:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
if not manifest.get("configItems"):
|
||||
report.warning(
|
||||
where,
|
||||
"uses `getConfig(...)` but `module.json` declares no `configItems`",
|
||||
)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
# The JSON-level checks elsewhere will report the actual cause.
|
||||
pass
|
||||
|
||||
|
||||
# ───────────────────────── entry point ─────────────────────────────────
|
||||
|
||||
def _load_json(report: Report, path: Path, where: str) -> dict[str, Any] | None:
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except FileNotFoundError:
|
||||
report.error(where, f"file not found: {path}")
|
||||
except json.JSONDecodeError as e:
|
||||
report.error(where, f"invalid JSON: {e.msg} (line {e.lineno}, col {e.colno})")
|
||||
except UnicodeDecodeError:
|
||||
report.error(where, "file must be UTF-8")
|
||||
return None
|
||||
|
||||
|
||||
def main(repo_root: Path) -> int:
|
||||
modules_dir = repo_root / "modules"
|
||||
if not modules_dir.is_dir():
|
||||
print(f"❌ {modules_dir} does not exist", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
report = Report()
|
||||
|
||||
# 1. Parse + validate the registry.
|
||||
registry = _load_json(report, modules_dir / "registry.json", "registry.json")
|
||||
registry_entries: list[dict[str, Any]] = []
|
||||
if isinstance(registry, dict):
|
||||
registry_entries = _validate_registry(report, registry)
|
||||
|
||||
# 2. Walk module folders.
|
||||
folders = sorted(
|
||||
p for p in modules_dir.iterdir()
|
||||
if p.is_dir() and not p.name.startswith(".")
|
||||
)
|
||||
|
||||
folder_paths = {p.name for p in folders}
|
||||
registry_paths = {e["path"] for e in registry_entries if _is_str(e.get("path"))}
|
||||
custom_paths = {e["path"] for e in registry_entries if _is_str(e.get("path")) and e.get("sourceType", "CUSTOM") != "CHROME_EXTENSION"}
|
||||
|
||||
# Folders missing a registry entry.
|
||||
for orphan in folder_paths - registry_paths:
|
||||
report.error(f"modules/{orphan}", "module folder has no entry in registry.json")
|
||||
# Registry entries pointing at non-existent folders (CHROME_EXTENSION entries don't need folders).
|
||||
for ghost in custom_paths - folder_paths:
|
||||
report.error("registry.json", f"entry refers to missing folder modules/{ghost}/")
|
||||
|
||||
# 3. Per-folder validation + cross-file consistency.
|
||||
entries_by_path = {e["path"]: e for e in registry_entries if _is_str(e.get("path")) and e.get("sourceType", "CUSTOM") != "CHROME_EXTENSION"}
|
||||
for folder in folders:
|
||||
_validate_folder_layout(report, folder)
|
||||
_validate_main_js(report, folder)
|
||||
_validate_icon_coherence(report, folder, entries_by_path.get(folder.name))
|
||||
|
||||
manifest_path = folder / "module.json"
|
||||
manifest = _load_json(report, manifest_path, f"modules/{folder.name}/module.json")
|
||||
if isinstance(manifest, dict):
|
||||
_validate_module_json(report, folder, manifest)
|
||||
entry = entries_by_path.get(folder.name)
|
||||
if entry:
|
||||
_validate_cross_consistency(report, entry, manifest, folder)
|
||||
|
||||
print(report.render())
|
||||
return 0 if report.ok() else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
repo = Path(__file__).resolve().parents[3]
|
||||
sys.exit(main(repo))
|
||||
122
.github/scripts/ui_design_allowlist.txt
vendored
Normal file
122
.github/scripts/ui_design_allowlist.txt
vendored
Normal file
@ -0,0 +1,122 @@
|
||||
# DruksCode UI design-system historical debt baseline.
|
||||
# Format: relative/path.kt|rule_key|allowed_count
|
||||
# Regenerate only after intentionally accepting a new migration checkpoint.
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/ActivationCodeCard.kt|raw_corner_shape|12
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/ActivationCodeCard.kt|raw_surface|6
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/ActivationCodeCard.kt|raw_button|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/AutoRefreshCountdownOverlay.kt|raw_corner_shape|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/AutoRefreshCountdownOverlay.kt|raw_surface|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/AutoStartCard.kt|raw_corner_shape|6
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/AutoStartCard.kt|raw_card|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/AutoStartCard.kt|raw_surface|6
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/BgmCard.kt|raw_surface|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/BgmSelector.kt|raw_corner_shape|3
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/BgmSelector.kt|raw_surface|9
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/BrowserDisguiseConfigCard.kt|raw_corner_shape|10
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/BrowserDisguiseConfigCard.kt|raw_surface|5
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/CodeSnippetSelector.kt|raw_corner_shape|10
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/CodeSnippetSelector.kt|raw_surface|8
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/ColorPickerDialog.kt|raw_corner_shape|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/DnsConfigCard.kt|raw_corner_shape|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/DnsConfigCard.kt|raw_card|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/EncryptionConfigCard.kt|raw_corner_shape|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/EncryptionConfigCard.kt|raw_card|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/EnhancedActivationDialog.kt|raw_corner_shape|6
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/EnhancedActivationDialog.kt|raw_surface|3
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/IconGeneratorDialog.kt|raw_corner_shape|7
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/IconGeneratorDialog.kt|raw_surface|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/IconLibraryDialog.kt|raw_corner_shape|4
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/IconLibraryDialog.kt|raw_card|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/IconLibraryDialog.kt|raw_surface|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/IconPickerWithLibrary.kt|raw_surface|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/IsolationConfigCard.kt|raw_corner_shape|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/IsolationConfigCard.kt|raw_card|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/LanguageSelector.kt|raw_card|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/LiquidGlassEffect.kt|hardcoded_color|4
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/LongPressMenu.kt|hardcoded_color|6
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/LongPressMenu.kt|raw_corner_shape|10
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/LongPressMenu.kt|raw_surface|8
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/LrcEditorDialog.kt|raw_corner_shape|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/LrcEditorDialog.kt|raw_surface|5
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/ManualLrcAligner.kt|raw_corner_shape|3
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/ManualLrcAligner.kt|raw_card|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/ManualLrcAligner.kt|raw_surface|4
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/NotificationConfigCard.kt|raw_corner_shape|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/NotificationConfigCard.kt|raw_card|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/OnlineMusicSearchDialog.kt|raw_corner_shape|5
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/OnlineMusicSearchDialog.kt|raw_card|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/OnlineMusicSearchDialog.kt|raw_surface|3
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/PremiumComponents.kt|raw_button|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/QrCodeShareDialog.kt|raw_corner_shape|6
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/QrCodeShareDialog.kt|raw_card|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/RuntimeScreenComponents.kt|raw_corner_shape|5
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/RuntimeScreenComponents.kt|raw_card|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/RuntimeScreenComponents.kt|raw_surface|4
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/SampleProjectCard.kt|raw_corner_shape|3
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/Shimmer.kt|hardcoded_color|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/Shimmer.kt|raw_corner_shape|7
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/StatusBarImageCropper.kt|raw_corner_shape|3
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/StatusBarImageCropper.kt|raw_card|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/TypedSampleProjectCard.kt|raw_corner_shape|3
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/VideoTrimmer.kt|raw_surface|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/VirtualNavigationBar.kt|hardcoded_color|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/VirtualNavigationBar.kt|raw_surface|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/DkcCodeEditorDialog.kt|raw_surface|5
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/DkcErrorView.kt|raw_corner_shape|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/DkcErrorView.kt|raw_surface|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/announcement/AnnouncementTemplates.kt|hardcoded_color|28
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/announcement/AnnouncementTemplates.kt|raw_corner_shape|13
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/announcement/AnnouncementTemplates.kt|raw_card|3
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/announcement/AnnouncementTemplates.kt|raw_surface|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/components/announcement/AnnouncementTemplates.kt|raw_button|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/AboutScreen.kt|raw_corner_shape|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/AiSettingsScreen.kt|raw_corner_shape|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/AiSettingsScreen.kt|raw_surface|10
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/BatchImportDialog.kt|raw_surface|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateAppScreen.kt|raw_corner_shape|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateAppScreen.kt|raw_surface|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateAppScreen.kt|raw_button|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateAppSplashCard.kt|raw_surface|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateAppUserScripts.kt|raw_corner_shape|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateAppUserScripts.kt|raw_card|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateAppUserScripts.kt|raw_surface|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateAppWebViewCards.kt|raw_corner_shape|13
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateAppWebViewCards.kt|raw_surface|8
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateFrontendAppScreen.kt|raw_corner_shape|3
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateFrontendAppScreen.kt|raw_surface|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateGalleryAppScreen.kt|raw_corner_shape|7
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateGalleryAppScreen.kt|raw_surface|5
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateGoAppScreen.kt|raw_corner_shape|7
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateGoAppScreen.kt|raw_surface|8
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateHtmlAppScreen.kt|raw_corner_shape|6
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateHtmlAppScreen.kt|raw_surface|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateMediaAppScreen.kt|raw_corner_shape|16
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateMediaAppScreen.kt|raw_surface|7
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateMultiWebAppScreen.kt|raw_corner_shape|10
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateMultiWebAppScreen.kt|raw_card|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateMultiWebAppScreen.kt|raw_surface|3
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateMultiWebAppScreen.kt|raw_button|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateOfflinePackScreen.kt|raw_corner_shape|3
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreateOfflinePackScreen.kt|raw_surface|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreatePhpAppScreen.kt|raw_corner_shape|3
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreatePhpAppScreen.kt|raw_surface|4
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreatePythonAppScreen.kt|raw_corner_shape|8
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/CreatePythonAppScreen.kt|raw_surface|9
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/DeviceDisguiseCard.kt|raw_corner_shape|8
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/DeviceDisguiseCard.kt|raw_surface|6
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/HomeScreen.kt|raw_corner_shape|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/HomeScreen.kt|raw_surface|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/HostsAdBlockScreen.kt|raw_surface|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/InstallProjectDepsCard.kt|raw_corner_shape|2
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/LinuxEnvironmentScreen.kt|raw_corner_shape|11
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/PermissionConfigScreen.kt|raw_corner_shape|5
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/PermissionConfigScreen.kt|raw_surface|5
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/PlayStoreScreen.kt|hardcoded_color|7
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/PlayStoreScreen.kt|raw_corner_shape|3
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/PlayStoreScreen.kt|raw_surface|4
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/PortManagerScreen.kt|raw_corner_shape|6
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/PortManagerScreen.kt|raw_surface|3
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/RuntimeDepsScreen.kt|raw_surface|1
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/StatsScreen.kt|raw_corner_shape|4
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/StatsScreen.kt|raw_surface|4
|
||||
app/src/main/java/br/com/drukstech/codeapp/ui/screens/WordPressSettingsScreen.kt|raw_corner_shape|3
|
||||
127
.github/workflows/android-ci.yml
vendored
Normal file
127
.github/workflows/android-ci.yml
vendored
Normal file
@ -0,0 +1,127 @@
|
||||
name: Android CI Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
# Group per ref AND event type: routine pushes to the same branch still cancel
|
||||
# their superseded check runs, but a workflow_dispatch packaging run is never
|
||||
# cancelled by a merge push landing on the same ref mid-build.
|
||||
group: android-ci-${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' }}
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- name: Download code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Prepare Java
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Prepare Android tools
|
||||
uses: android-actions/setup-android@v3
|
||||
with:
|
||||
packages: ""
|
||||
|
||||
- name: Prepare Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
cache-provider: basic
|
||||
|
||||
- name: Install needed tools
|
||||
run: |
|
||||
sdkmanager --install \
|
||||
"platform-tools" \
|
||||
"platforms;android-36" \
|
||||
"build-tools;36.0.0" \
|
||||
"cmake;3.22.1" \
|
||||
"ndk;28.2.13676358"
|
||||
|
||||
- name: Run checks
|
||||
run: |
|
||||
./gradlew --no-daemon --no-configuration-cache \
|
||||
-Dkotlin.compiler.execution.strategy=in-process \
|
||||
-PskipShellTemplateSync=true \
|
||||
-PallowDebugSignedRelease=true \
|
||||
:app:compileStandardDebugKotlin \
|
||||
:shell:compileDebugKotlin \
|
||||
:app:testStandardDebugUnitTest \
|
||||
:app:checkConfigFieldDrift
|
||||
|
||||
package-apks:
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- name: Download code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Prepare Java
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Prepare Android tools
|
||||
uses: android-actions/setup-android@v3
|
||||
with:
|
||||
packages: ""
|
||||
|
||||
- name: Prepare Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
cache-provider: basic
|
||||
|
||||
- name: Install needed tools
|
||||
run: |
|
||||
sdkmanager --install \
|
||||
"platform-tools" \
|
||||
"platforms;android-36" \
|
||||
"build-tools;36.0.0" \
|
||||
"cmake;3.22.1" \
|
||||
"ndk;28.2.13676358"
|
||||
|
||||
- name: Build APKs
|
||||
run: |
|
||||
./gradlew --no-daemon --no-configuration-cache \
|
||||
-Dkotlin.compiler.execution.strategy=in-process \
|
||||
-PallowDebugSignedRelease=true \
|
||||
:app:assembleStandardDebug \
|
||||
:app:assembleStandardRelease \
|
||||
:app:assembleGplayDebug \
|
||||
:app:assembleGplayRelease \
|
||||
:shell:assembleDebug \
|
||||
:shell:assembleRelease
|
||||
|
||||
- name: Upload standard APKs
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: standard-apks-DEBUG-SIGNED-do-not-distribute-${{ github.run_number }}
|
||||
path: |
|
||||
app/build/outputs/apk/standard/**/*.apk
|
||||
shell/build/outputs/apk/**/*.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
- name: Upload gplay APKs
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gplay-apks-br.com.drukstech.codeapp.play-DEBUG-SIGNED-do-not-distribute-${{ github.run_number }}
|
||||
path: app/build/outputs/apk/gplay/**/*.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
78
.github/workflows/docs-deploy.yml
vendored
Normal file
78
.github/workflows/docs-deploy.yml
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
# Builds the VitePress documentation site under docs/ and deploys it to
|
||||
# GitHub Pages. On pull requests the site is only built (to catch breakage);
|
||||
# on push to main and on manual dispatch it is built and deployed.
|
||||
name: Docs Deploy
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "docs/**"
|
||||
- ".github/workflows/docs-deploy.yml"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "docs/**"
|
||||
- ".github/workflows/docs-deploy.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: pages-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Download code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: npm
|
||||
cache-dependency-path: docs/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: docs
|
||||
run: npm ci
|
||||
|
||||
- name: Build site
|
||||
working-directory: docs
|
||||
run: npm run build
|
||||
|
||||
- name: Upload Pages artifact
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs/.vitepress/dist
|
||||
|
||||
deploy:
|
||||
if: github.event_name != 'pull_request'
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
# Only this job touches Pages: the build job runs on pull_requests (including
|
||||
# same-repo branches) and executes third-party dependency postinstall code, so it
|
||||
# must not hold Pages/OIDC write capabilities.
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
49
.github/workflows/modules-check.yml
vendored
Normal file
49
.github/workflows/modules-check.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
name: Module Market Check
|
||||
|
||||
# Runs the module catalog validator at `.github/scripts/ci/validate_modules.py`.
|
||||
# Triggered for any change touching the catalog or the validator itself,
|
||||
# whether on a PR or a direct push to main. Other paths bypass this job
|
||||
# so contributors who don't touch `modules/` aren't blocked by it.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'modules/**'
|
||||
- '.github/scripts/ci/validate_modules.py'
|
||||
- '.github/scripts/ci/generate_submissions.py'
|
||||
- '.github/workflows/modules-check.yml'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'modules/**'
|
||||
- '.github/scripts/ci/validate_modules.py'
|
||||
- '.github/scripts/ci/generate_submissions.py'
|
||||
- '.github/workflows/modules-check.yml'
|
||||
# Manual run from the Actions tab — handy when adjusting the validator
|
||||
# rules without a real catalog change to push.
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: modules-check-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
validate-modules:
|
||||
name: Validate modules/
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- name: Download code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Use system Python 3
|
||||
# Ubuntu runners ship a recent Python 3 — no setup-python step needed.
|
||||
run: python3 --version
|
||||
|
||||
- name: Run module market validator
|
||||
run: python3 .github/scripts/ci/validate_modules.py
|
||||
124
.github/workflows/modules-publish.yml
vendored
Normal file
124
.github/workflows/modules-publish.yml
vendored
Normal file
@ -0,0 +1,124 @@
|
||||
name: Module Market Publish
|
||||
|
||||
# Regenerates `modules/submissions.json` after a PR is merged into main.
|
||||
#
|
||||
# This file is what tells the in-app market who submitted each module
|
||||
# and when. Modules without a corresponding entry are deliberately
|
||||
# hidden from the catalog — that's how the "only show merged PRs"
|
||||
# guarantee is enforced. We regenerate from scratch on every relevant
|
||||
# push so a missing entry can never silently outlive a revert.
|
||||
#
|
||||
# main is branch-protected (requires a PR + the "check" status), and the
|
||||
# default GITHUB_TOKEN can't push to it directly. So instead of pushing,
|
||||
# this workflow commits the regenerated file to a fixed working branch
|
||||
# and opens (or updates) a PR, then queues an auto-merge so the catalog
|
||||
# stays in sync without any manual step.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'modules/**'
|
||||
- '.github/scripts/ci/generate_submissions.py'
|
||||
- '.github/workflows/modules-publish.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: modules-publish
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Generate submissions.json
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# We need the full history so `git log --diff-filter=A` can find
|
||||
# the introducing commit for each module folder.
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Use system Python 3
|
||||
run: python3 --version
|
||||
|
||||
- name: Generate submissions.json
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Legacy parameter: the generator records every human direct-push
|
||||
# author (modules only reach main via review or maintainer push)
|
||||
# and only excludes bot identities.
|
||||
MAINTAINERS: shiaho777
|
||||
run: |
|
||||
python3 .github/scripts/ci/generate_submissions.py \
|
||||
--owner "${GITHUB_REPOSITORY%%/*}" \
|
||||
--repo "${GITHUB_REPOSITORY##*/}" \
|
||||
--token "$GITHUB_TOKEN"
|
||||
|
||||
- name: Open PR for regenerated submissions.json
|
||||
env:
|
||||
# Prefer a PAT (secret PAT_TOKEN) when available: a GITHUB_TOKEN-
|
||||
# pushed branch can't trigger other workflows (GitHub anti-loop), so
|
||||
# the required "check" never reports on the resulting PR and it can't
|
||||
# merge. A PAT-pushed branch triggers android-ci normally. Fall back to
|
||||
# GITHUB_TOKEN so the workflow still runs without a PAT configured.
|
||||
GH_TOKEN: ${{ secrets.PAT_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
GIT_AUTH_USER: x-access-token
|
||||
GIT_AUTH_PWD: ${{ secrets.PAT_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
HAS_PAT: ${{ secrets.PAT_TOKEN != '' }}
|
||||
BRANCH: ci/regenerate-submissions
|
||||
run: |
|
||||
if git diff --quiet -- modules/submissions.json; then
|
||||
echo "No changes to submissions.json"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# The main branch is protected (requires a PR + the "check" status),
|
||||
# and the default GITHUB_TOKEN can't push there directly. Push the
|
||||
# regenerated file to a long-lived working branch and let a PR land it.
|
||||
git checkout -B "$BRANCH"
|
||||
git add modules/submissions.json
|
||||
git commit -m "chore(modules): regenerate submissions.json"
|
||||
|
||||
REPO="github.com/${GITHUB_REPOSITORY}"
|
||||
if [ "$HAS_PAT" = "true" ]; then
|
||||
git push --force-with-lease "https://${GIT_AUTH_USER}:${GIT_AUTH_PWD}@${REPO}" "$BRANCH"
|
||||
else
|
||||
git push --force-with-lease origin "$BRANCH"
|
||||
fi
|
||||
|
||||
# Reuse an existing open PR if one is already tracking this branch.
|
||||
PR_NUMBER=$(gh pr list --head "$BRANCH" --base main --state open \
|
||||
--json number --jq '.[0].number // empty')
|
||||
|
||||
TITLE="chore(modules): regenerate submissions.json"
|
||||
BODY="Auto-generated by the \`Module Market Publish\` workflow on push to \`main\`. No manual changes — this only updates \`modules/submissions.json\` so the in-app market catalog stays in sync."
|
||||
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
PR_NUMBER=$(gh pr create --head "$BRANCH" --base main \
|
||||
--title "$TITLE" --body "$BODY")
|
||||
else
|
||||
gh pr edit "$PR_NUMBER" --title "$TITLE" --body "$BODY"
|
||||
fi
|
||||
|
||||
# When using a PAT, the branch push already triggered android-ci, so
|
||||
# wait for its "check" to report then enable auto-merge (GitHub merges
|
||||
# automatically once all required checks pass). With GITHUB_TOKEN there
|
||||
# is no check to wait on, so the PR is left for a manual merge.
|
||||
if [ "$HAS_PAT" = "true" ]; then
|
||||
gh pr merge "$PR_NUMBER" --squash --auto --delete-branch \
|
||||
|| echo "::warning::Auto-merge setup failed; PR #$PR_NUMBER left open for manual merge."
|
||||
else
|
||||
echo "No PAT_TOKEN configured; PR #$PR_NUMBER left open for manual merge (GITHUB_TOKEN can't satisfy the required check)."
|
||||
fi
|
||||
75
.gitignore
vendored
Normal file
75
.gitignore
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
*.apk
|
||||
*.aar
|
||||
*.ap_
|
||||
*.aab
|
||||
*.dex
|
||||
*.class
|
||||
*.so
|
||||
*.tar.gz
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
release/
|
||||
.gradle/
|
||||
build/
|
||||
.vscode/
|
||||
.claude/
|
||||
.zcode/
|
||||
.workbuddy/
|
||||
.workbuddy-ai/
|
||||
local.properties
|
||||
gradle/config.properties
|
||||
gradle/gradle-daemon-jvm.properties
|
||||
rule.md
|
||||
CLAUDE.md
|
||||
*.iml
|
||||
.idea/
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/gradle.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/dictionaries
|
||||
.idea/libraries
|
||||
.idea/caches
|
||||
.idea/modules.xml
|
||||
.idea/misc.xml
|
||||
.idea/vcs.xml
|
||||
.navigation/
|
||||
captures/
|
||||
.externalNativeBuild/
|
||||
.cxx/
|
||||
*.jks
|
||||
*.keystore
|
||||
*.log
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
*.kotlin_module
|
||||
obj/
|
||||
app/src/main/jniLibs/arm64-v8a/libphp.so
|
||||
/out/
|
||||
.idea/caches/
|
||||
.idea/libraries/
|
||||
.idea/shelf/
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/.name
|
||||
.idea/compiler.xml
|
||||
.idea/copyright/profiles_settings.xml
|
||||
.idea/encodings.xml
|
||||
.idea/scopes/scope_settings.xml
|
||||
.idea/vcs.xml
|
||||
tmp_apk_inspect/
|
||||
bugreport-*.zip
|
||||
test-haze.kt
|
||||
restore
|
||||
server/
|
||||
admin/
|
||||
node_modules/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.kotlin/
|
||||
370
AGENTS.md
Normal file
370
AGENTS.md
Normal file
@ -0,0 +1,370 @@
|
||||
# AGENTS.md
|
||||
|
||||
Instructions for coding agents working in this repository.
|
||||
|
||||
## Code style
|
||||
|
||||
- Do what you believe is right. Make the change complete and correct, not the smallest possible diff. If a fix calls for refactoring, renaming, or touching multiple files, do it.
|
||||
- Match the patterns and conventions already in the surrounding code.
|
||||
- Do not add copyright or license headers unless asked.
|
||||
|
||||
## Project layout
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| `app/` | Full builder host: editor UI, export pipeline, runtimes, preview. |
|
||||
| `shell/` | Runtime template. Built to `app/src/main/assets/template/webview_shell.apk` via `:shell:assembleRelease` + `:app:syncShellTemplateApk`. |
|
||||
| `clone-host/` | Host-side APK clone / identity reshape support library. Its DEX asset generation (`syncCloneHostDex`) is deliberately disabled (`enabled = false`, AV false-positive mitigation, e0d2d4d6) — `AppCloner` runs fail-soft without the asset. |
|
||||
| `modules/` | Module Market catalog (`registry.json` + per-module folders). |
|
||||
| `docs/` | VitePress documentation site (guide / developer / extensions, EN + ZH), published to https://shiaho777.github.io/web-to-app/ by `.github/workflows/docs-deploy.yml`. Site URL paths map 1:1 to files under `docs/` (`/zh/...` → `docs/zh/...`). |
|
||||
| `scripts/` | Build helpers and gates (`check_config_field_drift.py`). |
|
||||
|
||||
Runtime Kotlin is authored under `app/` and synced into `shell` by `syncShellRuntimeSources`. Edit the `app/` source once; do not permanently fork copies under shell.
|
||||
|
||||
User-facing product docs: `README.md`, `.github/docs/README_CN.md`, `.github/CONTRIBUTING.md`, `modules/README.md`. The published documentation site is https://shiaho777.github.io/web-to-app/ (source: `docs/`, deployed by `.github/workflows/docs-deploy.yml`).
|
||||
|
||||
## How the main pieces connect
|
||||
|
||||
```text
|
||||
Editor (Compose screens in app/)
|
||||
↔ data models (WebApp, configs)
|
||||
↔ export factory (ApkConfig / ApkConfigJsonFactory)
|
||||
↔ ApkBuilder / ApkBuildCache → signed generated APK
|
||||
|
||||
app/ sources
|
||||
→ syncShellRuntimeSources → shell DEX → webview_shell.apk (template)
|
||||
|
||||
Generated APK runtime
|
||||
DruksCodeApplication → ShellModeManager → load assets JSON config
|
||||
→ WebViewManager / runtime servers (Node/PHP/Python/Go/WordPress)
|
||||
```
|
||||
|
||||
Mental model:
|
||||
|
||||
- **Host preview** runs `:app` with all classes on the main classpath.
|
||||
- **Generated APK** runs the shell template classes (full runtime synced from `app/`), reading config from assets JSON via `ShellModeManager`.
|
||||
- A flag in the editor is useless at export unless it flows through **model → ApkConfig JSON → shell config → runtime code**.
|
||||
|
||||
## i18n
|
||||
|
||||
- Host UI strings live in `app/src/main/java/br/com/drukstech/codeapp/core/i18n/Strings.kt` (split across `Strings` / `StringsA` … `StringsE`).
|
||||
- **All** user-visible strings must be inline `when (Strings.lang)` blocks covering all 10 languages: Chinese, English, Arabic, Portuguese, Spanish, French, German, Russian, Japanese, Korean. `when(lang)` blocks may never use `else ->` — `AppStringsResourceConsistencyTest` and `StringsKtTranslationParityTest` enforce this.
|
||||
- **Never** load user-visible text via `context.getString(R.string.*)` / `stringResource(R.string.*)`. `res/values/strings.xml` holds only `translatable="false"` resources (e.g. `app_name`) and no locale `values-*/` directories exist, so a localized resource string could never cover the 10 languages and would silently fall back to the default `values/` (Chinese). Use `Strings.xxx` (or `Strings.funName(arg)` for parameterised strings — see `linuxEnvInstalledToast(name)` for the pattern). Tests gate this: `kotlin source never references R string for user-visible text`, plus `values strings xml only holds non-localised resources` and `no locale values dirs or grouped app strings files exist` which block resurrecting resource-based strings.
|
||||
- `R.string` is reserved for `translatable="false"` non-localised resources only (e.g. `app_name`).
|
||||
- Prefer adding properties on the existing split objects; match surrounding style.
|
||||
|
||||
## Android and packaging constraints
|
||||
|
||||
- Generated apps keep a low `targetSdk` (28) on the shell path because they rely on on-device fork+exec runtimes. Do not raise shell targetSdk casually.
|
||||
- The host app targets SDK 35 (antivirus reputation); SELinux W^X therefore blocks host-side exec of downloaded runtimes. `RuntimeExecPolicy` (`core/linux`) gates those previews by probing the installed targetSdk — generated APKs (always 28) pass unconditionally. Node preview (JNI via native libs) is unaffected.
|
||||
- Launch runtime / toolchain processes through `HostProcessLauncher` (`core/linux`): plain `ProcessBuilder` fork+exec where the platform allows it (generated APKs, targetSdk 28), the user-mode static exec loader (`StaticExecProcess`, memfd-based) on W^X hosts (targetSdk ≥ 29), or a degraded error when no static exec bridge is available. A raw `Runtime.exec` on a downloaded binary crashes the host app on W^X devices (#795).
|
||||
- Avoid new third-party dependencies unless strongly justified (`app/build.gradle.kts` / `shell/build.gradle.kts`). Prefer platform APIs and existing modules.
|
||||
- Notification push channels: Web Notification polyfill, polling, WebSocket, FCM (developer-owned Firebase config). Do not add OEM vendor push SDKs by default.
|
||||
- Foreground services and notification helpers must use `SafeNotificationChannels` (or equivalent fail-soft create). Channel creation failures must not crash FGS startup.
|
||||
- **One shell template:** `webview_shell.apk` from `:shell` release. Do not introduce a second template APK.
|
||||
- Export incremental rebuild lives in `app/.../apkbuilder` (`ApkBuildCache` + `ApkBuilder`):
|
||||
- Modes: `FULL` / `CONTENT_OVERLAY` / `REUSE_UNSIGNED`.
|
||||
- Template / entry identities must be **content-stable** (no mtime-based keys).
|
||||
- Encrypted builds always force a full rebuild.
|
||||
- Do not feed signed or renamed APKs back into full `modifyApk` as templates.
|
||||
- Port coordination: `PortManager` + `PortConflictMode` (`REASSIGN` / `AUTO_KILL` / `ALERT`) with real stop handlers. Local server runtimes must allocate through PortManager and clean up on stop.
|
||||
- Local server / Linux env DNS: fork+exec runtimes (Node / PHP / Python / Go / WordPress / Linux env) should wire through `LocalDnsBridgeProxy` when they need host DNS/proxy bridging.
|
||||
- Large runtime downloads use `NetworkModule.downloadClient` (extended timeouts), not the default short-lived client.
|
||||
- HTML / FRONTEND packaged shells need file-scheme access via `ShellWebViewConfig` (`allowFileAccess` / local-file detection). Do not regress pure file-based HTML loads.
|
||||
- Node.js export must embed `libnode_bridge.so`, `libnode.so` (16KB-aligned), and `libc++_shared.so` as native libs. Go export must embed `libgo_exec_loader.so`.
|
||||
- Gradle custom tasks (`syncCloneHostDex`, etc.) must be configuration-cache safe: capture `File`/`Provider` values at configuration time, do not reference `Project`/`android.sdkDirectory` inside task closures.
|
||||
|
||||
## Workflow
|
||||
|
||||
- Do not commit secrets, `local.properties`, keystores, or IDE/cache junk.
|
||||
- Do not create commits, push, open PRs, or file Issues unless the user asks to deliver / ship / push / open a PR (or equivalent).
|
||||
- When changing export or shell packaging, rebuild the template you touched.
|
||||
- When changing config fields, run `checkConfigFieldDrift` to catch model ↔ shell config name drift.
|
||||
|
||||
### Delivery (Issue + PR + CI)
|
||||
|
||||
Default target: [shiaho777/drukscode](https://github.com/shiaho777/web-to-app). Prefer a pull request over direct pushes to `main` when delivering code. Human-facing wording of the same loop lives in [CONTRIBUTING.md](.github/CONTRIBUTING.md); keep those docs in sync when this process changes.
|
||||
|
||||
**Language (required):** GitHub **Issues and PRs must be written in English** — titles, bodies, labels text you author, and delivery comments on the Issue/PR. Local chat with the user may be Chinese or any language; do not copy that language into Issue/PR text.
|
||||
|
||||
When the user asks to deliver a change, run the Issue → branch → PR → CI → merge loop end-to-end. Do not close the Issue until the PR is merged and CI is green.
|
||||
|
||||
---
|
||||
|
||||
## Shell template and runtime sync
|
||||
|
||||
### Dual runtime (preview vs export)
|
||||
|
||||
| | Host `:app` | Generated APK |
|
||||
|--|-------------|---------------|
|
||||
| DEX | All `app/src` classes | Shell sync include−exclude (full runtime set) |
|
||||
| Config | Editor / in-memory models | Assets JSON via `ShellModeManager` |
|
||||
| Template | Not used | `app/src/main/assets/template/webview_shell.apk` |
|
||||
|
||||
**Preview ≠ export** unless both paths stay valid:
|
||||
|
||||
1. Host still has the real implementation.
|
||||
2. Shell-synced code reads the config field from assets JSON at runtime.
|
||||
3. The field name in `ApkConfig` JSON matches the `@SerializedName` in `ShellModeManager` (Gson silently drops mismatches).
|
||||
|
||||
Most common failure: preview works; exported APK silently skips the feature because a config field name drifted between the export factory and the shell config class.
|
||||
|
||||
### Where each concern is edited
|
||||
|
||||
| Concern | Path |
|
||||
|---------|------|
|
||||
| What enters shell | `shell/build.gradle.kts` → `syncShellRuntimeSources` include/exclude |
|
||||
| Shell template build | `:shell:assembleRelease` + `:app:syncShellTemplateApk` |
|
||||
| Template output | `app/src/main/assets/template/webview_shell.apk` |
|
||||
| Config → shell JSON | `app/.../apkbuilder/ApkConfigJsonFactory.kt` |
|
||||
| Shell config types | `app/.../core/shell/ShellModeManager.kt` |
|
||||
| Runtime WebView config | `app/.../ui/shell/ShellWebViewConfig.kt` |
|
||||
| Config drift gate | `scripts/check_config_field_drift.py` → `:app:checkConfigFieldDrift` |
|
||||
| Shell minify policy | `shell/proguard-rules.pro` |
|
||||
|
||||
---
|
||||
|
||||
## Common change recipes
|
||||
|
||||
These are the default approaches for everyday work. Follow the chain end-to-end; stopping at UI or host-only code is how preview and export diverge.
|
||||
|
||||
### 1. Add or change a host UI string
|
||||
|
||||
1. Add the property in the correct `Strings*` split with all 10 languages.
|
||||
2. Reference it from Compose/UI the same way neighbors do.
|
||||
|
||||
### 2. Add an editor setting that must affect the generated APK
|
||||
|
||||
Trace and update **all** of:
|
||||
|
||||
1. Model (`WebApp` / nested config) and editor UI binding
|
||||
2. Export mapping (`ApkBuilder` / `ApkConfig` / `ApkConfigJsonFactory`)
|
||||
3. Shell config types (`ShellModeManager` / shell config data classes) if runtime reads them
|
||||
4. Runtime use site in shell-synced code
|
||||
5. Unit tests for export wiring when flags change
|
||||
6. **Coverage test update (REQUIRED).** When adding a new Boolean field to
|
||||
`WebViewConfig`, you MUST add it to `flipAllBooleans()` in
|
||||
`WebViewConfigBooleanCoverageTest.kt`. The test
|
||||
`flipAllBooleans covers every declared Boolean field` uses reflection to
|
||||
verify that every declared Boolean field is listed — if you forget, CI
|
||||
fails with a message naming the missing field. For non-Boolean fields, add
|
||||
a spot-check to `key non-Boolean WebViewConfig fields survive the export
|
||||
round-trip`. This is the safety net that catches "preview works, export
|
||||
broken" before it ships.
|
||||
7. **Shell UI parity (REQUIRED for per-type player screens).** Gallery / media /
|
||||
splash-style app types render through DIFFERENT composables in preview
|
||||
(`ui/gallery/`, `ui/media/`) vs generated APKs (`ui/shell/`). A flag that flows
|
||||
end-to-end still does nothing if the shell screen never reads it (gallery
|
||||
thumbnail bar shipped config + assets correctly and still rendered nothing,
|
||||
#781). `ShellUiParityTest.kt` enforces this: every model field read by host
|
||||
runtime UI must also be read by shell runtime UI. If you add a field and read
|
||||
it in a host player, the test fails naming the field until you port the
|
||||
behavior to shell or add an explicit `allow` entry with a reason. When adding
|
||||
a new per-type player pair, extend its `pairs` list the same way.
|
||||
|
||||
Missing any step usually yields: editor shows the switch, export ignores it, or export embeds config the runtime never reads.
|
||||
|
||||
### 3. Change shell runtime behavior used by every generated app
|
||||
|
||||
1. Edit the source under `app/` (shared runtime).
|
||||
2. Confirm the file is included by `syncShellRuntimeSources`.
|
||||
3. Rebuild shell template if you need to validate packaging.
|
||||
4. Keep changes surgical; shell has a low targetSdk and a thin dependency set.
|
||||
5. If you touch FGS / notification channel creation, fail soft via `SafeNotificationChannels`.
|
||||
|
||||
### 4. Add a host-only feature (editor, market, tooling)
|
||||
|
||||
1. Keep implementation under host-only packages (`core/apkbuilder`, host screens, sample/market, `core/host`, …).
|
||||
2. Do not pull host-only deps into `shell/build.gradle.kts`.
|
||||
|
||||
### 5. Touch APK export or incremental rebuild
|
||||
|
||||
1. Prefer `ApkBuildCache` / content hashes over timestamps.
|
||||
2. Encrypted builds stay full rebuild.
|
||||
3. Do not use signed outputs as templates.
|
||||
4. If template bytes change, ensure cache keys invalidate correctly.
|
||||
|
||||
### 6. Change notifications / engines / network hardening
|
||||
|
||||
1. Prefer existing channel abstractions (polyfill / polling / WebSocket / FCM).
|
||||
2. Do not add OEM push SDKs by default.
|
||||
3. FGS channel create paths must tolerate OEM/channel failures.
|
||||
|
||||
### 7. Module Market / `modules/`
|
||||
|
||||
1. Follow `modules/README.md` catalog layout (`registry.json` + module folders).
|
||||
2. Runtime consumption still goes through the extension/shell paths if it ships inside generated APKs.
|
||||
|
||||
### 8. Fix "works in preview, broken after export"
|
||||
|
||||
Checklist in order:
|
||||
|
||||
1. Did shell config JSON actually contain the field at runtime?
|
||||
2. Does the field name in `ApkConfig` JSON match the `@SerializedName` in shell config? Run `checkConfigFieldDrift`.
|
||||
3. Is the runtime use site shell-synced (not host-only)?
|
||||
4. For adblock: confirm `adBlockEnabled` mapping, host filter rebuild from cached subscriptions, and export rule compile without wiping host state.
|
||||
5. Rebuild template after sync changes (stale template is a frequent miss).
|
||||
|
||||
### 9. Local server runtime / download path
|
||||
|
||||
1. Allocate ports through `PortManager` with the configured conflict policy; implement real stop handlers.
|
||||
2. Wire fork+exec processes into `LocalDnsBridgeProxy` when they need host DNS/proxy env.
|
||||
3. Use `NetworkModule.downloadClient` for large dependency / engine / runtime downloads.
|
||||
4. Launch processes through `HostProcessLauncher` (`core/linux`) so W^X hosts (targetSdk ≥ 29) degrade to the user-mode static exec loader instead of crashing.
|
||||
|
||||
### 10. Node.js / Go export
|
||||
|
||||
1. Node.js: ensure `injectNodeJsNativeLibs` embeds `libnode_bridge.so` + `libnode.so` (16KB-aligned via `ElfAligner16k`) + `libc++_shared.so`. Node binary resolution prefers `nativeLibraryDir`, falls back to download cache.
|
||||
2. Go: ensure `injectGoExecLoaderNativeLib` embeds `libgo_exec_loader.so`.
|
||||
3. Go host-side builds never run the multi-process `go build` driver: `GoDirectBuilder` replays `compile`/`asm`/`link` as single-shot processes through `HostProcessLauncher` (user-mode static exec loader on W^X hosts), with a content-keyed persistent package archive cache (#796).
|
||||
4. `NodeService` runs in a dedicated `:nodejs` OS process so V8 lifecycle is isolated from the host.
|
||||
|
||||
### 11. Change a feature that has an Agent tool
|
||||
|
||||
The in-app Agent exposes up to 57 tools — 52 base + 2 plan-mode + 3 imagery (imagery only load with an image-capable model; see `ToolRegistryFactory.build()`) — that wrap host service classes. When you change a feature, trace the tool chain:
|
||||
|
||||
```text
|
||||
LLM response (tool_calls)
|
||||
→ AgentEngine.executeToolCall()
|
||||
→ PermissionPrompter (write tools ask user; read-only run silently)
|
||||
→ Tool.execute(args, ctx)
|
||||
→ Service class (ApkBuilder, AppExporter, PortManager, EngineManager, AdBlocker, …)
|
||||
→ ToolResult → back to LLM
|
||||
```
|
||||
|
||||
Checklist when touching a feature that an Agent tool wraps:
|
||||
|
||||
1. **API signature drift.** If the service class constructor, method signature, or return type changes, update the tool's `execute()` in `app/.../agent/tool/builtin/`. A stale call compiles only if the old overload still exists; otherwise it's a build break.
|
||||
2. **`parametersSchema` ↔ `execute()` alignment.** The JSON schema advertised to the LLM must match what `execute()` actually reads from `args`. Adding a parameter to the service call without exposing it in the schema means the LLM can never pass it.
|
||||
3. **`description` accuracy.** The LLM decides *when* and *how* to call a tool solely from `description` + `parametersSchema`. If the feature's behavior changed, update the description text — a misleading description causes silent misuse.
|
||||
4. **`isReadOnly()` correctness.** `true` → runs without user confirmation; `false` → triggers `PermissionPrompter` dialog. If a tool gains write side-effects, flip it to `false`.
|
||||
5. **Registration.** New tools must be added to `ToolRegistryFactory.baseTools()` (grouped by domain comment). Forgotten registration = tool invisible to the LLM.
|
||||
6. **Removed features.** If a feature is deleted, remove or disable its tool. A tool that calls a dead class crashes at runtime inside the agent loop.
|
||||
7. **Host-only.** Agent tools live under `core/agent/` and are never shell-synced. Do not add them to `syncShellRuntimeSources`.
|
||||
|
||||
Key paths:
|
||||
|
||||
| Concern | Path |
|
||||
|---------|------|
|
||||
| Tool interface | `app/.../agent/tool/Tool.kt` |
|
||||
| Tool registry | `app/.../agent/tool/ToolRegistryFactory.kt` |
|
||||
| Tool context (services access) | `app/.../agent/tool/ToolContext.kt` |
|
||||
| Built-in tools (by domain) | `app/.../agent/tool/builtin/*.kt` |
|
||||
| Agent loop / execution | `app/.../agent/engine/AgentEngine.kt` |
|
||||
| Permission prompt (Channel-based) | `app/.../agent/permission/PermissionPrompter.kt` |
|
||||
| LLM provider (SSE streaming) | `app/.../agent/llm/OpenAiCompatProvider.kt` |
|
||||
|
||||
### 12. Change editor config-card UI (Compose)
|
||||
|
||||
The editor screens are built from `DkcSettingCard`s that share one visual grammar. The activation-card series (issue #567: PRs #568 → #571 → #573 → #574) took four attempts because each round invented layout instead of copying the neighbours, and each round was only compile-checked. A short prompt like "改一下 XX 卡的 UI" still means: follow these rules.
|
||||
|
||||
**Copy, don't invent.** Before writing any card, open the reference implementation on the same screen and mirror it element by element:
|
||||
|
||||
| You are writing… | Copy from (`CreateAppWebViewCards.kt`) |
|
||||
|------------------|----------------------------------------|
|
||||
| Toggle-headed feature card | `FullscreenModeCard` |
|
||||
| Collapsible card with `DkcChoiceRow` header | `BrowserAdvancedConfigCard` |
|
||||
| Radio group (single-choice) | PWA offline-strategy selector in `sectionOfflinePerformance` |
|
||||
| Sub-toggles / fields inside an expanded zone | `staticAssetPack` block, proxy block |
|
||||
| Nested conditional content | proxy MANUAL/PAC `AnimatedVisibility` swap |
|
||||
|
||||
Inputs and dialogs follow the same grammar: text inputs are `PremiumTextField`, list-manager dialogs are `Dialog` + `DkcCard` + embedded `TopAppBar` (see `AdBlockSubscriptionSelectorDialog`), and `ActivationCodeCard`'s offline-policy rows are another canonical radio reference.
|
||||
|
||||
Hard rules learned the hard way:
|
||||
|
||||
1. **Two sanctioned card shapes.** Toggle header: `DkcSettingCard { DkcToggleRow(header); AnimatedVisibility(enabled, CardExpandTransition/CardCollapseTransition) { Column { DkcSectionDivider(); full-bleed rows separated by more dividers } } }`. Collapse header: `DkcSettingCard { Column { DkcChoiceRow(header); AnimatedVisibility(expanded) { Column(padding(horizontal = DkcSpacing.RowHorizontal, vertical = DkcSpacing.ContentGap), spacedBy(DkcSpacing.ContentGap)) { … } } } }`.
|
||||
2. **Alignment grid.** `DkcToggleRow` / `DkcChoiceRow` are full-bleed rows carrying their own 16dp padding. Anything that is *not* a row — text fields, radios, group labels, buttons, notes — must sit inside a `Column(padding(horizontal = DkcSpacing.RowHorizontal, vertical = DkcSpacing.ContentGap))` zone. Sub-toggles inside a zone double-indent (16dp zone + 16dp row); that matches the reference, do not "fix" it. Group labels are `Text(style = labelMedium, color = primary)`, not `DkcSection`.
|
||||
3. **Radio groups** are exactly: `Text(labelMedium, primary)` label, then `Row(fillMaxWidth().clip(MaterialTheme.shapes.small).clickable { … }.padding(vertical = 4.dp)) { RadioButton; Spacer(4.dp); Text(bodySmall) }`. Never wrap a radio group in `DkcSection`, never attach per-option prose.
|
||||
4. **Card headers carry title + icon only.** No subtitle on the header row, no free-floating description text next to it. Subtitles belong on child rows that genuinely need one (e.g. 显示状态栏).
|
||||
5. **Expansion state ≠ feature state.** Never bind a section's `isExpanded` / `AnimatedVisibility(visible=…)` to the feature's `enabled` flag — expanding a panel must never switch the feature on. Section collapse state is its own `remember { mutableStateOf }`.
|
||||
6. **Conditional sub-blocks** (mode swaps, dependent fields) use `AnimatedVisibility` with `CardExpandTransition` / `CardCollapseTransition`, never bare `if` inside the card body.
|
||||
7. **Compile ≠ verified.** After any card UI change, build + install on the emulator and check the rendered card: `uiautomator dump` element bounds, compare left edges / row heights of the changed card against its neighbours on the same screen (they must share the same content columns), plus a screenshot pass. Content a few dp off the grid is invisible in code review and obvious on screen.
|
||||
8. **If a UI rework PR gets "this doesn't match the other cards" feedback**, the fix is realignment to these patterns, not further invention.
|
||||
|
||||
---
|
||||
|
||||
## Easy-to-miss points
|
||||
|
||||
- **Keyboard avoidance below API 30 requires the classic window path.** Android 10 and lower have no native IME-inset dispatch: an edge-to-edge window (`decorFitsSystemWindows = false`) is never resized for the keyboard and reports zero IME insets, so no `softInputMode` value helps there. `WindowHelper.applyImmersiveFullscreen` therefore keeps the decor fitting system windows on the RESIZE keyboard path below API 30 (system `SOFT_INPUT_ADJUST_RESIZE` works) and degrades TRANSPARENT/IMAGE status-bar styles to solid colors on that path. Do not re-enable edge-to-edge unconditionally for those devices (#613; #634 flipped only the softInputMode bits and fixed nothing — its Robolectric test passed because it never asserted the layout flags).
|
||||
- **Shared sources are authored in `app/`.** Editing only a file under `shell/src` is usually wrong; it will be overwritten on sync or diverge from host.
|
||||
- **Config field names drift.** Editor model, `ApkConfig`, JSON factory, and shell config must stay aligned; Gson silently drops unknown/missing fields. Run `checkConfigFieldDrift`.
|
||||
- **Low targetSdk (28) and fork/exec runtimes** constrain "modernize the shell SDK" changes.
|
||||
- **Incremental export cache** keys must be content-based; mtime and resigned APKs create false hits/misses.
|
||||
- **HTML/FRONTEND file access.** Packaged local-file shells must have `allowFileAccess = true` (forced in `buildWebViewBlock` and `ShellWebViewConfig`); do not regress pure file-based HTML loads.
|
||||
- **Node native libs.** Exported NODEJS_APP needs `libnode_bridge.so` + `libnode.so` + `libc++_shared.so`; missing any causes `loadNode` / `loadJniBridge` failure at runtime.
|
||||
- **16KB page alignment.** `libnode.so` and other large ELF natives must be 16KB-aligned (`ElfAligner16k`) for Android 15+ devices; `node_bridge.cpp` / `node_launcher.c` enable 16KB app-compat before `dlopen`.
|
||||
- **Node JNI output bridge.** `NodeJniOutputBridge` is a stable class referenced by native code; keep its `-keep` proguard rule so R8 does not rename `onOutput`.
|
||||
- **Crashing FGS when notification channel creation fails.** Always use `SafeNotificationChannels` for channel creation.
|
||||
- **Adblock is wired for preview + export.** Do not wipe host filter state during export; the host AdBlocker serves preview and the compiled rule set ships in the APK.
|
||||
- **Runtime permissions are feature-driven.** `RuntimePermissionSync` derives the permission list from enabled features; do not revert to a static template.
|
||||
- **Splash preview media path.** Preview reads splash media from the host filesystem (`splashMediaPath`); export packages it into assets. Do not hardcode `assets/splash_media.*` as the only source.
|
||||
- **Host player UI ≠ shell player UI.** Preview players (`ui/gallery/`, `ui/media/`) and generated-APK players (`ui/shell/`) are separate composables sharing only the config. Port every visible behavior across (thumbnail bar, background, keep-screen-on, orientation) and let `ShellUiParityTest` verify the flags; config flowing end-to-end is necessary but not sufficient.
|
||||
- **Port conflict policy.** Local server runtimes must allocate through `PortManager` and clean up on stop; do not bind ports directly.
|
||||
- **Agent tool ↔ service drift.** When a service class API changes, the corresponding Agent tool in `core/agent/tool/builtin/` must be updated in the same PR. A stale tool either fails to compile or silently passes wrong arguments at runtime. Check `ToolRegistryFactory.baseTools()` for the full tool list.
|
||||
- **Editor card UI grammar.** Config-screen cards share one layout grammar (recipe 12): rows are full-bleed, non-row content sits in 16dp-padded zones, expansion never toggles the feature, and card UI is verified on the emulator — not just compiled.
|
||||
|
||||
---
|
||||
|
||||
## Forbidden / high-risk mistakes
|
||||
|
||||
- Second shell template APK
|
||||
- Excluding a shell-synced class but leaving imports/constructors in shell-synced sources
|
||||
- Re-enabling shell R8 obfuscation / aggressive shrink without testing exported apps
|
||||
- Putting host-only tools back into shell sync for convenience
|
||||
- OEM push SDKs or unjustified heavy dependencies in shell
|
||||
- Feeding signed/renamed APKs into template/modify paths
|
||||
- Crashing FGS when notification channel creation fails
|
||||
- Committing secrets, keystores, or local machine config
|
||||
- Regressing HTML/FRONTEND file access in packaged shells
|
||||
- Shipping a host player-screen feature without its shell counterpart (preview-only UI)
|
||||
- Shipping NODEJS_APP without `libnode_bridge.so` / `libnode.so` / `libc++_shared.so`
|
||||
- Skipping 16KB alignment for large ELF natives
|
||||
- Inventing editor card layout/spacing/animations instead of copying the neighbouring cards' patterns, or shipping card UI verified only by compilation
|
||||
|
||||
---
|
||||
|
||||
## Verify commands
|
||||
|
||||
```bash
|
||||
./gradlew :shell:assembleRelease :app:syncShellTemplateApk --no-configuration-cache
|
||||
./gradlew :app:compileStandardDebugKotlin -x syncCloneHostDex --no-configuration-cache
|
||||
./gradlew :app:checkConfigFieldDrift --no-configuration-cache
|
||||
python3 scripts/check_config_field_drift.py
|
||||
```
|
||||
|
||||
Use these when you change shell membership, export packaging, or config fields. For host-only UI/string work, targeted compile on `:app` is usually enough.
|
||||
|
||||
CI note: the PR `check` job compiles only `:shell:compileDebugKotlin` (debug variant) and skips template sync (`-PskipShellTemplateSync=true`). The shell **release** variant (R8 / proguard) and the template pipeline are exercised only by the manual `workflow_dispatch` packaging job — after touching `shell/proguard-rules.pro` or shell packaging, run the first command locally before pushing.
|
||||
|
||||
Related focused tests often worth running after nearby edits: `ApkBuildCacheTest`, `AdBlockerHostRuntimeTest`, `AdBlockExportWiringTest`, `PortManagerTest`, `BuildInputPreflightTest`, `GoBuildEnvironmentTest`, `RuntimePermissionSyncTest`.
|
||||
|
||||
---
|
||||
|
||||
## Implementation snapshot
|
||||
|
||||
Landed:
|
||||
|
||||
- Single shell template (`webview_shell.apk`) from `:shell` release, full runtime synced from `app/`
|
||||
- Incremental `ApkBuildCache` (`FULL` / `CONTENT_OVERLAY` / `REUSE_UNSIGNED`); encrypted builds always full
|
||||
- Notification channels: polyfill, polling, WebSocket, FCM (BYO Firebase) via existing abstractions
|
||||
- `SafeNotificationChannels` fail-soft path for FGS
|
||||
- `PortManager` conflict policies + real stop handlers across Node/PHP/Python/Go/WordPress
|
||||
- `LocalDnsBridgeProxy` wiring for local server runtimes (including Node.js)
|
||||
- Runtime downloads via `NetworkModule.downloadClient`
|
||||
- Adblock preview + export wiring restored
|
||||
- HTML/FRONTEND file-access for packaged local shells
|
||||
- Node.js export: `libnode_bridge.so` + 16KB-aligned `libnode.so` + `libc++_shared.so`; 16KB app-compat before dlopen; stable `NodeJniOutputBridge` JNI callback
|
||||
- Go export: `libgo_exec_loader.so` embedded; in-app build ENOSPC handling + GOTMPDIR relocation
|
||||
- Go host-side builds on W^X hosts: `GoDirectBuilder` replays `compile`/`asm`/`link` as single-shot processes through `HostProcessLauncher` (user-mode static exec loader), with a content-keyed persistent package archive cache (#796)
|
||||
- Multi-web: gallery/media site sources embedded into the APK and resolved at runtime; nested site sources degrade to a URL instead of aborting the build (#798, #792)
|
||||
- Gallery playback: shuffleOnLoop, rememberPosition, overview grid, thumbnail bar, pinch-to-zoom viewer, with host/shell parity enforced by `ShellUiParityTest` (#781, #786, #801)
|
||||
- Untrusted bitmap decodes bounded (oversized-image crash fix) (#788)
|
||||
- Backup/restore coverage aligned with the current storage layout; restarts only when local files change, with throttled progress callbacks (#790, #794)
|
||||
- NativeBridge enabled by default for newly created apps (#805)
|
||||
- Runtime permission sync (feature-driven)
|
||||
- Splash preview media path fallback
|
||||
- Config field drift detection (`checkConfigFieldDrift`)
|
||||
- Module Market: Chrome Web Store live search + GreasyFork browse
|
||||
- Code editor find-and-replace
|
||||
- Security hardening sweep: TLS-fingerprint bridge validates upstream certs (system + custom CAs + hostname) and its local CA is signature-verified with no error-type fallback; JS bridges are caller/origin/scheme-gated (NativeBridge CORS bypass, GM bridge, MV3 `ChromeHostPermissions`); MITM proxy host-allowlisted and CA key wrapped at rest; zip extraction routed through `util/SafeZip`; concurrent exports serialized per package with per-package work dirs; multi-web server-runtime site sources degrade to URL; PHP/Python/WP embed failures fail the build; error pages and `TranslateBridge` callbacks JSON-escaped; keystore password sidecars excluded from backups
|
||||
- Agent tool system: up to 57 tools — 52 base + 2 plan-mode + 3 imagery (image-capable models only) — covering app lifecycle, config templates, ports/engine, hosts/runtime, stats/modifier/import, build env/Play, modules, files, and imagery, with Channel-based permission prompting, per-section SSE parse resilience, and plan mode; runtime/build-env tools surface `localExecAllowed` so the LLM knows targetSdk>=29 hosts cannot exec app-storage binaries
|
||||
24
LICENSE
Normal file
24
LICENSE
Normal file
@ -0,0 +1,24 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <https://unlicense.org>
|
||||
365
README.md
Normal file
365
README.md
Normal file
@ -0,0 +1,365 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" width="96" alt="DruksCode icon" />
|
||||
|
||||
# DruksCode
|
||||
|
||||
### Build Android APKs from web projects, directly on your phone.
|
||||
|
||||
**An on-device APK workshop that goes far beyond URL wrapping — it can fork and exec full server runtimes, ship a hardened anti-censorship network stack, sign bundles for Google Play, and run MV3 browser extensions, all without a PC or a remote build server.**
|
||||
|
||||
**English** · [简体中文](.github/docs/README_CN.md)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/shiaho777/web-to-app/stargazers">
|
||||
<img src="https://img.shields.io/github/stars/shiaho777/drukscode?style=for-the-badge" alt="Stars">
|
||||
</a>
|
||||
<a href="https://github.com/shiaho777/web-to-app/network/members">
|
||||
<img src="https://img.shields.io/github/forks/shiaho777/drukscode?style=for-the-badge" alt="Forks">
|
||||
</a>
|
||||
<a href="https://github.com/shiaho777/web-to-app/releases/latest">
|
||||
<img src="https://img.shields.io/github/v/release/shiaho777/drukscode" alt="Latest Version">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/22597?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-22597" target="_blank" rel="noopener noreferrer">
|
||||
<img src="https://trendshift.io/api/badge/trendshift/repositories/22597/weekly?language=Kotlin" alt="shiaho777%2Fdrukscode | Trendshift" width="250" height="55">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.swiftproxy.net/?ref=shiaho">
|
||||
<img src=".github/assets/sponsors/swiftproxy-logo-full.png" alt="Swiftproxy" width="220" />
|
||||
</a><br/>
|
||||
<a href="https://www.swiftproxy.net/?ref=shiaho"><b>Sponsored by Swiftproxy</b></a> — Swiftproxy · Reliable Residential Proxies for Web Automation
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#what-makes-webtoapp-different">What's different</a> ·
|
||||
<a href="#what-you-can-build">What you can build</a> ·
|
||||
<a href="#capability-overview">Capability overview</a> ·
|
||||
<a href="#full-feature-map">Feature map</a> ·
|
||||
<a href="#module-market">Module market</a> ·
|
||||
<a href="#documentation">Documentation</a> ·
|
||||
<a href="#architecture">Architecture</a> ·
|
||||
<a href="#build-from-source">Build</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<img src=".github/assets/social-preview.png" width="90%" alt="DruksCode: My Apps home, the app list with the main toolbox (Agent, Extension Modules, Port Manager), per-app APK actions (Edit, Create Shortcut, Build/Share/Export APK), and the About screen — all running on an Android phone" />
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## What makes DruksCode different
|
||||
|
||||
Most "website to app" tools stop at wrapping a URL in a WebView. DruksCode is closer to a pocket-sized APK workshop, and the hard parts are exactly where it diverges:
|
||||
|
||||
- **It runs real server runtimes on-device.** Node.js, PHP, Python, Go, and WordPress are fork+exec'd as native binaries straight from app storage — like Termux, packaged into an installable APK. URL-wrapper tools cannot do this at all.
|
||||
- **It ships a hardened, anti-censorship network stack.** DNS-over-HTTPS, TLS fingerprint spoofing (Chrome / Firefox / Safari JA3 templates) with a local MITM bridge, Encrypted Client Hello (ECH) on both engines to encrypt the SNI, per-app proxies, and CORS bypass for locked-down SPAs.
|
||||
- **The whole build is self-contained.** Binary AXML/ARSC patching, permission pruning, V1/V2/V3 signing, and Google Play-ready AAB export all happen inside the app via `apksig` — no remote build queue, no PC.
|
||||
- **It stays extensible after shipping.** Add JS/CSS modules, Tampermonkey-style userscripts, or MV3 Chrome extensions (live-searched and installed from the Chrome Web Store) without rebuilding the host.
|
||||
- **The host UI speaks 10 languages out of the box.** Chinese, English, Arabic (RTL), Portuguese, Spanish, French, German, Russian, Japanese, and Korean — switch anytime in Settings; new in-app copy is maintained for all ten.
|
||||
|
||||
---
|
||||
|
||||
## Capability overview
|
||||
|
||||
A quick scan of what's in the box. Each links to the detailed feature map below.
|
||||
|
||||
| Area | Highlights |
|
||||
| --- | --- |
|
||||
| **Build targets** | Web · HTML · Frontend · WordPress · Node.js · PHP · Python · Go · Image · Video · Gallery · Multi-Web |
|
||||
| **Browser engines** | System WebView by default; optional GeckoView (Firefox) runtime |
|
||||
| **Network & anti-censorship** | DoH (7 providers), TLS fingerprint spoofing + MITM bridge, ECH, static/PAC/SOCKS5 proxies, CORS bypass |
|
||||
| **Privacy & hardening** | 50+ vector browser fingerprint disguise, resource encryption (AES-256-GCM), anti-debug, activation gating |
|
||||
| **Local runtimes** | Native Node.js 18.20, PHP 8.4 + Composer 2.10, Python 3.14, official Go 1.26, WordPress 7.x over SQLite |
|
||||
| **Extensions** | Built-in modules, userscripts with `GM_*`, MV3 Chrome extensions, live Chrome Web Store search |
|
||||
| **APK/AAB output** | On-device V1/V2/V3 signing, Google Play AAB export with targetSdk rewrite, keystore management |
|
||||
| **Agent** | Full-app automation via up to 57 tools: generate, build, export, manage ports/engines/runtimes, clone apps, ad-block rules, config templates, and more; auto-retry on 429/5xx |
|
||||
| **Host languages** | **10 UI languages** — 中文 · English · العربية · Português · Español · Français · Deutsch · Русский · 日本語 · 한국어 (Arabic RTL) |
|
||||
|
||||
---
|
||||
|
||||
## What you can build
|
||||
|
||||
| Input | Output | Good for |
|
||||
| --- | --- | --- |
|
||||
| Website URL | WebView-based APK | Landing pages, tools, dashboards, docs, internal systems |
|
||||
| HTML / static front-end | Localhost-backed APK | React, Vue, Vite, static builds, offline web apps |
|
||||
| Node.js / PHP / Python / Go | APK with an on-device local server | Small server apps, admin tools, demos, prototypes |
|
||||
| WordPress | APK running WordPress over local PHP + SQLite | Portable sites, theme/plugin demos, content packages |
|
||||
| Images / video / galleries | Media-focused APK | Albums, course materials, portfolios, offline viewers |
|
||||
| Multiple sites | Tab/card/feed/drawer multi-web APK | Link hubs, portals, app collections |
|
||||
| Installed APK | Rebranded clone or shortcut disguise | Icon/name/package experiments, repackaging research |
|
||||
|
||||
---
|
||||
|
||||
## Full feature map
|
||||
|
||||
DruksCode has a large number of switches. The sections below group them by use case, kept collapsible so the top of the page stays scannable.
|
||||
|
||||
<details>
|
||||
<summary><b>🌐 Browser engine & networking</b></summary>
|
||||
|
||||
- **Dual engine** — System WebView by default, or an optional GeckoView (Firefox) runtime downloaded on first use.
|
||||
- **Kernel flavor disguise** — present as Chrome, Edge, Samsung Internet, Firefox, or Safari-style while keeping the real engine.
|
||||
- **Desktop mode**, custom User-Agent, and JS/CSS injection at document-start / end / idle.
|
||||
- **Popup handling** — same window, external browser, popup window, or block.
|
||||
- **Proxies** — static HTTP/HTTPS/SOCKS5, PAC, authentication, bypass rules, and a local HTTP-to-SOCKS bridge.
|
||||
- **DNS-over-HTTPS** — Cloudflare, Google, AdGuard, NextDNS, CleanBrowsing, Quad9, Mullvad, plus custom endpoints; strict or automatic modes.
|
||||
- **Encrypted Client Hello (ECH)** — encrypt the SNI in the TLS handshake on **both engines**: GeckoView via TRR + its ECH prefs, the system WebView via the MITM bridge's Chromium upstream (auto-downloaded on first use, embedded in exported APKs). Auto-wires DoH when toggled; a SOCKS upstream proxy takes precedence.
|
||||
- **TLS fingerprint spoofing** — impersonate Chrome 131 / Firefox 133 / Safari 18 JA3 profiles (or custom ciphers), served through a local TLS-MITM bridge so the outgoing ClientHello matches a real browser.
|
||||
- **CORS bypass** — on by default for static SPAs that call external APIs blocked by CORS; same-origin traffic is left alone, and CORS-only apps can use a lightweight `PrivateNetworkNativeBridgeAdapter` without the full Native Bridge surface.
|
||||
- **Failover** — automatic fallback to mirror URLs when the primary target is unreachable.
|
||||
- **PWA** offline cache strategies, custom error pages, per-app host overrides, and payment-scheme handlers.
|
||||
- **Compatibility toggles** — blob download interception, scroll memory, image repair, clipboard / orientation / notification polyfills, private-network bridging, and Native Bridge capability gates.
|
||||
- **Download location** — system Downloads, app-private storage, or a user-picked SAF folder, wired through the full packaging passthrough chain.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🛡️ Privacy, fingerprint defense & hardening</b></summary>
|
||||
|
||||
- **Browser fingerprint disguise across 50+ vectors** — User-Agent, WebGL, Canvas, AudioContext, ClientRects, timezone, language, memory, media devices, WebRTC, fonts, battery, permissions, performance, storage, notifications, CSS media, iframe propagation, and error-stack cleanup.
|
||||
- **Hosts-rule ad blocker** with cosmetic MutationObserver filtering, **20 built-in community filter lists** (EasyList, uBlock Origin, AdGuard, AdAway, plus 8 language-specific lists), per-source enable/disable/delete, named custom filter imports managed as cards and selectable per app, and custom subscription rules bundled into the APK.
|
||||
- **Resource encryption** (PBKDF2 + AES-256-GCM) for packaged config, HTML, media, and BGM; optional custom encryption password stronger than package/certificate-derived keys.
|
||||
- **Runtime hardening** when encryption is on — anti-debug, anti-Frida, DEX-tamper checks; threat response of log-only, silent exit, or randomized crash.
|
||||
- **WebView/content isolation** for storage, WebRTC, Canvas, Audio, WebGL, fonts, headers, and IP surfaces.
|
||||
- **Activation-code gating** — local verification, or your own HTTPS endpoint signed with EC P-256. See the [remote activation docs](.github/docs/remote-activation.md).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>📦 Local server runtimes (fork + exec on-device)</b></summary>
|
||||
|
||||
- **Node.js** (18.20.x) runs in a dedicated `:nodejs` OS process via a native `node_launcher` wrapper loading `libnode.so`; supports custom native `.node` extensions.
|
||||
- **PHP 8.4** from `pmmp/PHP-Binaries`, downloaded once on first use, with Composer 2.10.x and custom native extensions (`zend_extension`, `.so`).
|
||||
- **Python 3.14** — Flask, Django, FastAPI via uvicorn, Tornado, the built-in HTTP server; pip dependencies resolved into `.pypackages`, custom native extensions supported; binary names are versioned so future bumps do not hard-code paths.
|
||||
- **Go 1.26** — official Linux arm64 toolchain (`.tar.gz` from `dl.google.com`, USTC mirror for CN), on-device `go build` / `go mod` / `go run`, `vendor/` offline builds, static serving, and the native `go_exec_loader` wrapper; DNS and CA trust go through the same local JVM bridge used by PHP.
|
||||
- **WordPress 7.x** over local PHP + SQLite (`sqlite-database-integration`), with theme and plugin import.
|
||||
- **Linux Environment** screen manages toolchains and dependencies for Node, PHP, and Python.
|
||||
- **Port Manager** coordinates runtime ports across generated apps via broadcast receivers.
|
||||
- A **local DNS bridge proxy** (HTTP CONNECT in the Android JVM) gives runtimes working DNS resolution and outbound HTTP where the musl/packed binary can't reach the system resolver.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🧩 Extensions & automation</b></summary>
|
||||
|
||||
- **Built-in modules** — video download (YouTube / Bilibili / Douyin / Xiaohongshu extractors), video enhancer with YouTube cleanup (ad skip, max quality, background play, SponsorBlock), web analyzer, find-in-page, dark mode, privacy tools, content enhancer, element blocker, and YouTube launcher.
|
||||
- **Userscripts** — Greasemonkey/Tampermonkey-style `.user.js` with a `GM_*` bridge (storage, requests, styles, menu commands) and promise-based `GM.*` APIs gated by script grants.
|
||||
- **MV3 Chrome extension runtime** for manifest content scripts in isolated or main worlds, with `chrome.*` polyfills for runtime, storage, tabs, scripting, and declarative network-request parsing.
|
||||
- **In-app Chrome Web Store search** — browse and install browser extensions by keyword (or paste a store URL / extension ID), with offline fallback to manual import.
|
||||
- **Export codes** (`DKC1:` gzip + Base64) and QR sharing via ZXing.
|
||||
- **Agent** — a tool-calling assistant with up to 57 built-in tools covering the entire app surface: create/edit/build/export apps, manage ports and browser engines, install/clear runtimes, ad-block hosts rules, common-config templates, usage stats, app cloning, batch import, Play policy checks, and module development. Plan mode waits for user approval; automatic retry/backoff on 429/5xx.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>📱 App experience</b></summary>
|
||||
|
||||
- **Splash screens** — image or video, with skip behavior, trim ranges, and fixed orientation.
|
||||
- **Background music** — playlists with synced LRC lyrics, lyric animations, custom font/color/stroke/shadow, and online music search.
|
||||
- **Toolbar, status bar (light & dark), navigation, floating-window mode, and long-press menu styles.** The browser toolbar is a master toggle (off by default) with per-item buttons for title/URL/back/forward/refresh plus a native **find-in-page** bottom bar and a **console** panel for on-device debugging. Status bar color can follow theme, a custom color, full transparency, or **PAGE_TOP** (sample the page’s top pixels so the chrome matches the content).
|
||||
- **Download location mode** — system Downloads, app-private directory, or a custom SAF folder picked by the user.
|
||||
- **Announcement templates** for launch, interval, and no-network moments.
|
||||
- **Host app language** — switch the entire builder UI among 10 languages (中文 / English / العربية / Português / Español / Français / Deutsch / Русский / 日本語 / 한국어); Arabic is full RTL.
|
||||
- **Translation overlay** — 20 target languages via Google, MyMemory, LibreTranslate, or Lingva engines, with automatic failover across them (in-page translate for the *content* of generated apps, separate from host UI language).
|
||||
- **Print bridge** — intercept `window.print()` and blob/data-URL PDFs to the Android print framework / PDF export (with an onPageStarted re-inject fallback so late navigations stay hooked).
|
||||
- **Media Session bridge** — surface web media on the system media notification and lock-screen controls, with Bluetooth and Android Auto support.
|
||||
- **Notifications** — Web Notification polyfill, scheduled and persistent notifications with progress, URL-polling foreground service, deep links, boot auto-start, scheduled launch, and background-run service.
|
||||
- **Per-app usage stats** with Vico charts and URL health monitoring.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🔧 APK / AAB export & signing</b></summary>
|
||||
|
||||
- **Custom package name**, `versionName`, `versionCode`, icon, label, architecture target, and export format.
|
||||
- **Build-time permission injection** with unused permissions pruned from the template manifest.
|
||||
- **One-tap AAB export** — auto-builds the APK on demand, converts it to a Play-ready signed AAB with `targetSdk` rewritten to the Play-required level (currently 36) and protobuf metadata generated locally; cancellable mid-build.
|
||||
- **Keystore management** — create, import, export, delete, and certificate-fingerprint viewing; PKCS12/PFX/JKS/BKS import including Android Studio upload-key cases where store and key passwords differ.
|
||||
- **Signature schemes** — V1, V2, V3 independently controlled, with auto-fallback for legacy certificates; custom V1 signer filename for `META-INF/<name>.SF` / `.RSA`.
|
||||
- **Performance options** — image compression, WebP conversion, code minification, lazy loading, DNS prefetch, and preload hints.
|
||||
- **Full project and app-data backup/restore.**
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🗂 File manager & project tooling</b></summary>
|
||||
|
||||
- **File manager** — a single screen to view, share, install, open, and clear build outputs (APK builds, AAB exports, app clones, build logs) and a user-files directory, with a read-only build-log viewer.
|
||||
- **Website scraper** for offline packs — HTML, CSS, JS, images, fonts, `url()`, `srcset`, `@import`, path rewriting, same-domain limits, depth limits, and size limits; parallel streaming worker pool with main-thread progress callbacks.
|
||||
- **Multi-Web layouts** — tabs, cards, feeds, drawers, per-site icons/theme colors/extraction selectors/refresh intervals, and shared JS/CSS.
|
||||
- **Gallery apps** — categorized media, grid/list/timeline views, shuffle/single-loop, sorting, thumbnail bar, overlays, auto-next, and playback memory.
|
||||
- **App Modifier** — shortcut disguise or real binary clone with manifest/resource patching and re-signing.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🔬 Specialized tools & research features</b></summary>
|
||||
|
||||
- **Device disguise** is included for technical demonstration and must only be used with informed user consent.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Agent
|
||||
|
||||
DruksCode ships a built-in AI agent (open from **⋮ → Agent**) that can operate the entire app through natural-language conversation. Instead of tapping through menus, you describe what you want and the Agent executes it via a tool-calling loop backed by any LLM you configure in [AI Settings](#tech-stack) — Chat Completions, Anthropic Messages, or OpenAI Responses endpoints, plus Google Gemini and Ollama / LM Studio / VLLM locals.
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. You send a message (or attach an image for visual context).
|
||||
2. The LLM reasons and emits `tool_calls`.
|
||||
3. The Agent executes each tool on-device — read-only tools run immediately; write tools pop a permission dialog first.
|
||||
4. Results flow back to the LLM, which continues until the task is done or it asks you a clarifying question.
|
||||
|
||||
**Up to 57 built-in tools, grouped by domain (the 3 imagery tools load only with an image-capable model):**
|
||||
|
||||
| Domain | Examples |
|
||||
| --- | --- |
|
||||
| Files | Read, Write, Edit, Delete, List, Glob, Grep project files |
|
||||
| Apps | List, Get, Create, Update app configurations |
|
||||
| App lifecycle | Build APK/AAB, Export, Share, Create shortcut, Duplicate, Delete, Move to category |
|
||||
| Ports & engines | Scan/kill ports, check/select/delete browser engines (WebView, GeckoView) |
|
||||
| Runtimes | Status, install, and cache-clear for Node.js, PHP, Python, Go, WordPress, Linux env |
|
||||
| Ad-block | Rule counts, import/remove/enable/disable hosts subscriptions |
|
||||
| Stats & health | Usage statistics, URL health checks |
|
||||
| App modifier | List installed apps, clone/rebrand, batch import, export templates |
|
||||
| Build env & compliance | Initialize Linux build env, install components, Google Play policy checks |
|
||||
| Modules | List, create, update extension modules |
|
||||
| Interaction | Ask user questions (multi-select), plan mode (propose → approve → execute), todo tracking |
|
||||
|
||||
---
|
||||
|
||||
## Module market
|
||||
|
||||
DruksCode has a GitHub-backed module market for community JS/CSS extension modules. The catalog is just files in this repository, so contributions use a normal pull-request flow.
|
||||
|
||||
```
|
||||
modules/
|
||||
├── registry.json # app-facing catalog
|
||||
├── submissions.json # CI-generated PR / contributor metadata
|
||||
├── README.md # contributor guide
|
||||
└── <module-folder>/ # each module
|
||||
```
|
||||
|
||||
The app fetches both `registry.json` and `submissions.json` and only shows modules present in both, keeping the in-app catalog aligned with actually-merged PRs. The submissions file also records every contributor per module, so the catalog shows stacked avatars and a contributors leaderboard. Catalog files and module icons route through a global mirror first, with `raw.githubusercontent.com` and jsDelivr as automatic fallbacks, so the store loads fast everywhere (including mainland China).
|
||||
|
||||
- Users open **Extension Modules** and tap the storefront icon.
|
||||
- Contributors add a folder under `modules/`, update `registry.json`, and open a PR.
|
||||
- The default client cache is one hour, so merged modules propagate without an app update.
|
||||
|
||||
The high-level architecture lives here; the canonical submission rules, field schemas, reviewer checklist, and CI validation details live in [`modules/README.md`](modules/README.md).
|
||||
|
||||
The community market carries only JS/CSS extension modules. **Browser extensions (MV3)** are no longer a community catalog — instead the **Browser Extensions** tab searches the Chrome Web Store live: type a keyword, browse results, and install on demand through the existing CRX pipeline. Live search requires a network that can reach Google.
|
||||
|
||||
## Documentation
|
||||
|
||||
The official documentation site is published at **[shiaho777.github.io/web-to-app](https://shiaho777.github.io/web-to-app/)**, in English and 简体中文:
|
||||
|
||||
| Section | URL | Covers |
|
||||
| --- | --- | --- |
|
||||
| Guide | [/guide/introduction](https://shiaho777.github.io/web-to-app/guide/introduction) | Getting started, main screen, app types, per-app actions and common config, FAQ |
|
||||
| Developer | [/developer/](https://shiaho777.github.io/web-to-app/developer/) | Architecture, export pipeline, shell sync, config drift, i18n, change recipes |
|
||||
| Extensions | [/extensions/](https://shiaho777.github.io/web-to-app/extensions/) | JS/CSS modules, userscripts, Chrome MV3, API reference, publishing |
|
||||
| 简体中文 | [/zh/](https://shiaho777.github.io/web-to-app/zh/) | Full Chinese mirror of every page above |
|
||||
|
||||
## Architecture
|
||||
|
||||
- The repository has **three Gradle modules**: `app` (the full builder and host), `shell` (the runtime host embedded into generated APKs), and `clone-host` (host code for app cloning — compiled to a `classes.jar`, converted to DEX via d8, and bundled as an asset for `AppCloner`).
|
||||
- Runtime code is authored in `app` and synchronized into `shell`, so shared WebView/runtime behavior has one source of truth (`core/shell`, `core/webview`, `core/engine`, `core/extension`, `ui/shell`, etc.).
|
||||
- The APK builder patches template APKs at the binary AXML/ARSC level, injects config/resources, prunes permissions, and signs with `apksig`. A separate encrypted build path (`EncryptedApkBuilder`) offers resource encryption, shelling, and integrity checks.
|
||||
- Generated APKs pin `targetSdk = 28` deliberately (via the shell template) — it is what lets them fork+exec native runtimes (Node.js, PHP, Python, Go, WordPress) from app storage, a capability URL-wrapper tools lack. The host app itself targets 35 (antivirus engines flag low-targetSdk builds as legacy malware); SELinux W^X at that level blocks host-side preview of the exec-based runtimes, which degrade with an explicit message — Node.js preview (JNI) and every exported app are unaffected. For Play Store distribution the AAB exporter separately rewrites `targetSdk` to the Play-required level (currently 36). WebView-only app types (Web/HTML/Frontend/Gallery/Media/MultiWeb) can also opt to raise the standalone APK's `targetSdk` (34/35/36) from the APK export section; server-runtime types are gated to 28 because `targetSdk >= 29` enforces W^X and breaks fork+exec of the bundled binaries.
|
||||
- Server runtimes and the optional GeckoView native libraries (`.so` + `omni.ja`) are downloaded on first use rather than bundled into the base APK; the GeckoView API classes come from a gradle dependency, while the heavy native artifacts are fetched on demand.
|
||||
- The configuration center is `WebApp` (`data/model/WebApp.kt`) and its `*Config` classes — the single source of truth for all feature settings, carried through a full packaging passthrough chain into the generated APK.
|
||||
|
||||
## Tech stack
|
||||
|
||||
- Kotlin, Jetpack Compose, Material 3
|
||||
- Koin for dependency injection
|
||||
- Room 2.7.2 + KSP for persistence
|
||||
- OkHttp 4.12.0 + `okhttp-dnsoverhttps`
|
||||
- `com.android.tools.build:apksig` 8.3.0 for APK signing
|
||||
- BouncyCastle 1.78.1 for crypto and signing
|
||||
- `protobuf-javalite` 3.25.5 for AAB metadata
|
||||
- Firebase Cloud Messaging (BYO Firebase config) for push notifications
|
||||
- GeckoView as an optional browser engine (native libs downloaded on first use)
|
||||
- Coil for image/video/GIF loading
|
||||
- Haze for blur/glassmorphism UI
|
||||
- Gson for JSON serialization (shell config pipeline)
|
||||
- AndroidX Security Crypto + DataStore for stored secrets
|
||||
- Vico Compose-M3 for charts
|
||||
- ZXing for QR sharing
|
||||
- Apache Commons Compress + xz for project import and website scraping
|
||||
- Native C++ via JNI for `node_launcher` and `go_exec_loader`
|
||||
- Robolectric for unit tests
|
||||
|
||||
See [app/build.gradle.kts](app/build.gradle.kts) for the complete dependency list.
|
||||
|
||||
## Build from source
|
||||
|
||||
Requirements: Android Studio Hedgehog or newer, JDK 17. The Gradle wrapper pins Gradle 9.4.1.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/shiaho777/web-to-app.git
|
||||
cd drukscode
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
|
||||
For release builds, configure signing through `local.properties` and `app/build.gradle.kts`.
|
||||
|
||||
## Contributing
|
||||
|
||||
| Lane | What you do | Guide |
|
||||
| --- | --- | --- |
|
||||
| `modules/` | Publish a community module to the in-app market | [modules/README.md](modules/README.md) |
|
||||
| Issues | Report a bug or request a feature | [GitHub Issues](https://github.com/shiaho777/web-to-app/issues) |
|
||||
| Code | Fix a bug or build a feature in the Android client | [CONTRIBUTING.md](.github/CONTRIBUTING.md) |
|
||||
| `AGENTS.md` | Authoritative guide for AI coding agents and deep contributors (architecture, shell sync, export pipeline, config drift, packaging constraints) | [AGENTS.md](AGENTS.md) |
|
||||
|
||||
## Contact
|
||||
|
||||
Developed by **shiaho**.
|
||||
|
||||
| Platform | Link |
|
||||
| --- | --- |
|
||||
| GitHub | [github.com/shiaho777/web-to-app](https://github.com/shiaho777/web-to-app) |
|
||||
| Telegram | [t.me/webtoapp777](https://t.me/webtoapp777) |
|
||||
| X (Twitter) | [@shiaho777](https://x.com/shiaho777) |
|
||||
| Bilibili | [b23.tv/8mGDo2N](https://b23.tv/8mGDo2N) |
|
||||
| QQ Group | 1041130206 |
|
||||
|
||||
## License
|
||||
|
||||
[The Unlicense](LICENSE).
|
||||
|
||||
Advanced features such as device disguise are intended for technical demonstration and must only be used with informed user consent.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
[daoxe.com](https://daoxe.com) — AI API relay for personal use.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.swiftproxy.net/?ref=shiaho">
|
||||
<img src=".github/assets/sponsors/swiftproxy-banner-en.png" width="640" alt="Swiftproxy — Residential Proxies from $0.7/GB, ISP Proxies $6/IP, 10% off code: PROXY90" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.swiftproxy.net/?ref=shiaho"><b>Sponsored by Swiftproxy</b></a> — Swiftproxy provides 90M+ clean residential IPs across 220+ locations, supporting HTTP(S) and SOCKS5 proxies, flexible targeting, rotating and sticky sessions, and non-expiring traffic. It helps DruksCode users with reliable global access, location-based testing, web automation, and data collection. Free testing is available, with 10% off using code <code>PROXY90</code>.
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**Open source · Built for Android power users · Star to support the project**
|
||||
|
||||
</div>
|
||||
701
app/build.gradle.kts
Normal file
701
app/build.gradle.kts
Normal file
@ -0,0 +1,701 @@
|
||||
import java.util.Properties
|
||||
import java.util.zip.ZipFile
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.InputDirectory
|
||||
import org.gradle.api.tasks.OutputDirectory
|
||||
import org.gradle.api.tasks.PathSensitive
|
||||
import org.gradle.api.tasks.PathSensitivity
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("com.google.devtools.ksp")
|
||||
id("org.jetbrains.kotlin.plugin.compose")
|
||||
id("com.google.protobuf")
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
|
||||
val localProperties = Properties().apply {
|
||||
val file = rootProject.file("local.properties")
|
||||
if (file.exists()) {
|
||||
file.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
val releaseSigningStoreFile = localProperties.getProperty("signing.storeFile")
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { rootProject.file(it) }
|
||||
val hasReleaseSigningConfig = releaseSigningStoreFile?.isFile == true &&
|
||||
!localProperties.getProperty("signing.storePassword").isNullOrBlank() &&
|
||||
!localProperties.getProperty("signing.keyAlias").isNullOrBlank() &&
|
||||
!localProperties.getProperty("signing.keyPassword").isNullOrBlank()
|
||||
|
||||
android {
|
||||
|
||||
signingConfigs {
|
||||
if (hasReleaseSigningConfig) {
|
||||
create("release") {
|
||||
storeFile = releaseSigningStoreFile
|
||||
storePassword = localProperties.getProperty("signing.storePassword")
|
||||
keyAlias = localProperties.getProperty("signing.keyAlias")
|
||||
keyPassword = localProperties.getProperty("signing.keyPassword")
|
||||
}
|
||||
}
|
||||
}
|
||||
namespace = "br.com.drukstech.codeapp"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "br.com.drukstech.codeapp"
|
||||
minSdk = 23
|
||||
|
||||
targetSdk = 35
|
||||
versionCode = 62
|
||||
versionName = "2.6.0"
|
||||
buildConfigField("boolean", "SHELL_RUNTIME_ONLY", "false")
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
|
||||
ndk {
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
cppFlags += "-std=c++17"
|
||||
arguments += "-DANDROID_STL=c++_shared"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions += "distribution"
|
||||
productFlavors {
|
||||
create("standard") {
|
||||
// Sideloaded variant (GitHub releases, keeps `br.com.drukstech.codeapp` for the existing
|
||||
// update path). Identical to gplay in every way except the applicationId —
|
||||
// both inherit targetSdk 35 from defaultConfig and run the same code paths
|
||||
// (runtime capability gates key off the installed targetSdk, not the channel).
|
||||
}
|
||||
create("gplay") {
|
||||
// Google Play variant. Only Play's applicationId requirement differs
|
||||
// (`br.com.drukstech.codeapp` is registered on Google Play by another party); behavior,
|
||||
// targetSdk and every build rule are shared with the standard flavor.
|
||||
applicationId = "br.com.drukstech.codeapp.play"
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = file("src/main/cpp/CMakeLists.txt")
|
||||
version = "3.22.1"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
signingConfig = if (hasReleaseSigningConfig) {
|
||||
signingConfigs.getByName("release")
|
||||
} else {
|
||||
val allowDebugSigned = (project.findProperty("allowDebugSignedRelease") as? String) == "true"
|
||||
if (allowDebugSigned) {
|
||||
|
||||
signingConfigs.getByName("debug")
|
||||
} else {
|
||||
throw GradleException(
|
||||
"Release build has no valid signing config. Configure signing.storeFile / " +
|
||||
"signing.storePassword / signing.keyAlias / signing.keyPassword in local.properties " +
|
||||
"before assembling a release APK for distribution. Refusing to silently sign with " +
|
||||
"the debug key — a debug-signed release breaks upgrades for existing users " +
|
||||
"('signatures do not match'). For a throwaway debug-signed build (e.g. CI smoke " +
|
||||
"build, never distribute it), pass -PallowDebugSignedRelease=true."
|
||||
)
|
||||
}
|
||||
}
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = false
|
||||
}
|
||||
}
|
||||
|
||||
bundle {
|
||||
language {
|
||||
enableSplit = false
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
|
||||
disable += "NullSafeMutableLiveData"
|
||||
|
||||
disable += "ExpiredTargetSdkVersion"
|
||||
disable += "ExpiringTargetSdkVersion"
|
||||
disable += "OldTargetApi"
|
||||
|
||||
abortOnError = false
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
// Gecko omni.ja is downloaded on demand, never bundle it in host.
|
||||
excludes += "assets/omni.ja"
|
||||
excludes += "**/omni.ja"
|
||||
// BouncyCastle post-quantum experimental blobs (~1.2M), unused.
|
||||
excludes += "**/org/bouncycastle/pqc/**"
|
||||
// Django gettext sources (~5.9M raw): the Python runtime only reads
|
||||
// compiled .mo files, .po never ships in a working install.
|
||||
excludes += "**/*.po"
|
||||
}
|
||||
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
|
||||
excludes += "**/libxul.so"
|
||||
excludes += "**/libmozglue.so"
|
||||
excludes += "**/libgeckoffi.so"
|
||||
excludes += "**/libmozavutil.so"
|
||||
excludes += "**/libmozavcodec.so"
|
||||
|
||||
excludes += "**/libgkcodecs.so"
|
||||
excludes += "**/libminidump_analyzer.so"
|
||||
excludes += "**/libnss3.so"
|
||||
excludes += "**/libfreebl3.so"
|
||||
excludes += "**/libsoftokn3.so"
|
||||
excludes += "**/liblgpllibs.so"
|
||||
excludes += "**/libplugin-container.so"
|
||||
|
||||
excludes += "**/libcrypto_engine.so"
|
||||
|
||||
// Cronet natives are downloaded on demand / injected per-export, never bundled.
|
||||
excludes += "**/libcronet*.so"
|
||||
}
|
||||
}
|
||||
androidResources {
|
||||
// Keep "" semantics (include dot-dirs like .pypackages) while dropping
|
||||
// Django gettext sources (*.po, runtime reads .mo only) and Gecko's
|
||||
// omni.ja (downloaded on demand). packaging.resources.excludes does
|
||||
// NOT cover assets, hence aapt-level filtering (verified by APK audit).
|
||||
ignoreAssetsPattern = "*.po:*.ja"
|
||||
|
||||
localeFilters += listOf("zh", "en", "ar")
|
||||
}
|
||||
}
|
||||
|
||||
val shellTemplateOutput = project(":shell").layout.buildDirectory.file("outputs/apk/release/shell-release.apk")
|
||||
val skipShellTemplateSync = providers.gradleProperty("skipShellTemplateSync").map(String::toBoolean).orElse(false)
|
||||
|
||||
tasks.register<Copy>("syncShellTemplateApk") {
|
||||
description = "Builds the dedicated shell template APK and copies it into the app assets."
|
||||
group = "build"
|
||||
dependsOn(":shell:assembleRelease")
|
||||
from(shellTemplateOutput)
|
||||
into(file("src/main/assets/template"))
|
||||
rename { "webview_shell.apk" }
|
||||
}
|
||||
|
||||
tasks.matching { it.name == "preBuild" }.configureEach {
|
||||
if (!skipShellTemplateSync.get()) {
|
||||
dependsOn("syncShellTemplateApk")
|
||||
}
|
||||
}
|
||||
|
||||
val cloneHostAar = rootProject.layout.projectDirectory.file("clone-host/build/outputs/aar/clone-host-release.aar")
|
||||
val androidSdkDir = android.sdkDirectory
|
||||
|
||||
tasks.register<Copy>("syncCloneHostDex") {
|
||||
description = "Extracts classes.jar from clone-host AAR and converts it to a DEX asset for APK cloning."
|
||||
group = "build"
|
||||
dependsOn(":clone-host:assembleRelease")
|
||||
|
||||
enabled = false
|
||||
|
||||
val dexOutputDir = layout.projectDirectory.dir("src/main/assets/clone_host").asFile
|
||||
val intermediateDir = layout.buildDirectory.dir("intermediates/clone-host-extract")
|
||||
val sdkDir = androidSdkDir
|
||||
|
||||
doFirst {
|
||||
intermediateDir.get().asFile.mkdirs()
|
||||
dexOutputDir.mkdirs()
|
||||
}
|
||||
|
||||
from(cloneHostAar)
|
||||
into(intermediateDir)
|
||||
rename { "clone-host.aar" }
|
||||
|
||||
doLast {
|
||||
val aarFile = intermediateDir.get().file("clone-host.aar").asFile
|
||||
if (!aarFile.exists()) {
|
||||
throw GradleException("clone-host AAR not found at ${aarFile.absolutePath}")
|
||||
}
|
||||
|
||||
val classesJar = intermediateDir.get().file("classes.jar").asFile
|
||||
ZipFile(aarFile).use { zip ->
|
||||
val entry = zip.getEntry("classes.jar")
|
||||
?: throw GradleException("classes.jar not found in clone-host AAR")
|
||||
zip.getInputStream(entry).use { input ->
|
||||
classesJar.outputStream().use { output -> input.copyTo(output) }
|
||||
}
|
||||
}
|
||||
|
||||
val androidJar = sdkDir.resolve("platforms/android-36/android.jar")
|
||||
if (!androidJar.exists()) {
|
||||
throw GradleException("android.jar not found at ${androidJar.absolutePath}")
|
||||
}
|
||||
|
||||
val d8 = sdkDir.resolve("build-tools")
|
||||
.listFiles()?.maxByOrNull { it.name }
|
||||
?.resolve("d8")
|
||||
?: throw GradleException("d8 not found in build-tools")
|
||||
|
||||
dexOutputDir.mkdirs()
|
||||
|
||||
val process = ProcessBuilder(
|
||||
d8.absolutePath,
|
||||
"--release",
|
||||
"--min-api", "23",
|
||||
"--lib", androidJar.absolutePath,
|
||||
"--output", dexOutputDir.absolutePath,
|
||||
classesJar.absolutePath
|
||||
).redirectErrorStream(true).start()
|
||||
|
||||
val output = process.inputStream.bufferedReader().readText()
|
||||
if (process.waitFor() != 0) {
|
||||
throw GradleException("d8 failed to dex clone-host: $output")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.matching { it.name == "preBuild" }.configureEach {
|
||||
dependsOn("syncCloneHostDex")
|
||||
}
|
||||
|
||||
tasks.register("testClasses") {
|
||||
group = "verification"
|
||||
description = "Compatibility alias for JVM-style test class compilation in the Android app module."
|
||||
dependsOn("compileDebugUnitTestSources")
|
||||
}
|
||||
|
||||
tasks.register("unitTestClasses") {
|
||||
group = "verification"
|
||||
description = "Compatibility alias for Android unit test class compilation in the Android app module."
|
||||
dependsOn("compileDebugUnitTestSources")
|
||||
}
|
||||
|
||||
abstract class SyncNativeExecutableJniLibsTask : DefaultTask() {
|
||||
@get:Input
|
||||
abstract val variantName: org.gradle.api.provider.Property<String>
|
||||
|
||||
@get:Input
|
||||
abstract val buildTypeName: org.gradle.api.provider.Property<String>
|
||||
|
||||
@get:Input
|
||||
abstract val executableName: org.gradle.api.provider.Property<String>
|
||||
|
||||
@get:Input
|
||||
abstract val packagedLibraryName: org.gradle.api.provider.Property<String>
|
||||
|
||||
@get:InputDirectory
|
||||
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||
abstract val cxxRoot: DirectoryProperty
|
||||
|
||||
@get:OutputDirectory
|
||||
abstract val outputDir: DirectoryProperty
|
||||
|
||||
@TaskAction
|
||||
fun sync() {
|
||||
val cxxRootDir = cxxRoot.asFile.get()
|
||||
if (!cxxRootDir.exists()) {
|
||||
throw GradleException("CXX output not found for ${variantName.get()}: ${cxxRootDir.absolutePath}")
|
||||
}
|
||||
|
||||
val executableTargets = cxxRootDir.walkTopDown()
|
||||
.filter { file ->
|
||||
file.isFile &&
|
||||
file.name == executableName.get() &&
|
||||
file.parentFile?.parentFile?.name == "obj"
|
||||
}
|
||||
.toList()
|
||||
|
||||
if (executableTargets.isEmpty()) {
|
||||
throw GradleException("${executableName.get()} artifacts not found for ${variantName.get()} under ${cxxRootDir.absolutePath}")
|
||||
}
|
||||
|
||||
val outputRoot = outputDir.get().asFile
|
||||
outputRoot.deleteRecursively()
|
||||
outputRoot.mkdirs()
|
||||
|
||||
executableTargets.forEach { binary ->
|
||||
val abi = binary.parentFile.name
|
||||
val destFile = outputRoot.resolve("$abi/${packagedLibraryName.get()}")
|
||||
destFile.parentFile.mkdirs()
|
||||
binary.copyTo(destFile, overwrite = true)
|
||||
destFile.setExecutable(true, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the patched musl dynamic linker (libmusl-linker.so) into generated
|
||||
* jniLibs for arm64-v8a / x86_64. The linker carries the W^X exec bridge:
|
||||
* app_data library fds that SELinux refuses to exec-map are copied into an
|
||||
* executable memfd and mapped from there, restoring Python host previews on
|
||||
* targetSdk>=29 builds. Source provenance: upstream musl tarball +
|
||||
* scripts/patches/musl-1.2.5-dkc-exec-bridge.patch +
|
||||
* scripts/musl-bridge/dkc_mulxc3.c (NDK compiler-rt drops x86_80 routines).
|
||||
*/
|
||||
abstract class BuildMuslBridgeTask : DefaultTask() {
|
||||
@get:Input
|
||||
abstract val muslVersion: org.gradle.api.provider.Property<String>
|
||||
|
||||
@get:InputFile
|
||||
@get:PathSensitive(PathSensitivity.NONE)
|
||||
abstract val patchFile: RegularFileProperty
|
||||
|
||||
@get:InputFile
|
||||
@get:PathSensitive(PathSensitivity.NONE)
|
||||
abstract val x80ShimFile: RegularFileProperty
|
||||
|
||||
@get:InputDirectory
|
||||
@get:PathSensitive(PathSensitivity.NONE)
|
||||
abstract val ndkToolchainDir: DirectoryProperty
|
||||
|
||||
@get:Internal
|
||||
abstract val workDir: DirectoryProperty
|
||||
|
||||
@get:OutputDirectory
|
||||
abstract val outputDir: DirectoryProperty
|
||||
|
||||
private fun run(cmd: List<String>, cwd: File? = null, env: Map<String, String> = emptyMap()) {
|
||||
val proc = ProcessBuilder(cmd).apply {
|
||||
if (cwd != null) directory(cwd)
|
||||
environment().putAll(env)
|
||||
redirectErrorStream(true)
|
||||
}.start()
|
||||
val out = proc.inputStream.bufferedReader().readText()
|
||||
val code = proc.waitFor()
|
||||
if (code != 0) throw GradleException("command failed ($code): ${cmd.joinToString(" ")}\n$out")
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
fun build() {
|
||||
val tc = ndkToolchainDir.get().asFile
|
||||
if (!File(tc, "bin/clang").isFile) {
|
||||
throw GradleException("NDK toolchain not found at $tc (set NDKVersion or install the NDK)")
|
||||
}
|
||||
val libccDir = File(tc, "lib/clang")
|
||||
.listFiles { f -> f.isDirectory }?.map { File(it, "lib/linux") }.orEmpty()
|
||||
fun libcc(name: String): String =
|
||||
libccDir.firstOrNull { File(it, name).isFile }?.resolve(name)?.absolutePath
|
||||
?: throw GradleException("compiler-rt $name not found under $tc")
|
||||
|
||||
val work = workDir.get().asFile.apply { mkdirs() }
|
||||
val version = muslVersion.get()
|
||||
val tarball = File(work, "musl-$version.tar.gz")
|
||||
if (!tarball.isFile) {
|
||||
run(
|
||||
listOf(
|
||||
"curl", "-fsSL", "--max-time", "180", "-o", tarball.absolutePath,
|
||||
"https://musl.libc.org/releases/musl-$version.tar.gz"
|
||||
)
|
||||
)
|
||||
}
|
||||
val src = File(work, "musl-$version")
|
||||
src.deleteRecursively()
|
||||
run(listOf("tar", "xzf", tarball.absolutePath, "-C", work.absolutePath))
|
||||
run(listOf("patch", "-p1", "-d", src.absolutePath, "--input", patchFile.get().asFile.absolutePath))
|
||||
File(src, "src/math/x86_64").also { it.mkdirs() }
|
||||
.resolve(x80ShimFile.get().asFile.name).run {
|
||||
x80ShimFile.get().asFile.copyTo(this, overwrite = true)
|
||||
}
|
||||
|
||||
val clang = File(tc, "bin/clang").absolutePath
|
||||
val strip = File(tc, "bin/llvm-strip").absolutePath
|
||||
val jobs = Runtime.getRuntime().availableProcessors().toString()
|
||||
val targets = listOf(
|
||||
"arm64-v8a" to ("aarch64-linux-musl" to "libclang_rt.builtins-aarch64-android.a"),
|
||||
"x86_64" to ("x86_64-linux-musl" to "libclang_rt.builtins-x86_64-android.a")
|
||||
)
|
||||
val out = outputDir.get().asFile
|
||||
out.deleteRecursively()
|
||||
for ((abi, target) in targets) {
|
||||
val (triple, rt) = target
|
||||
val buildDir = File(work, "build-$abi").apply { deleteRecursively(); mkdirs() }
|
||||
val libccPath = libcc(rt)
|
||||
val configureEnv = mapOf(
|
||||
"CC" to "$clang --target=$triple",
|
||||
"CROSS_COMPILE" to "${File(tc, "bin/llvm-").absolutePath}",
|
||||
"CFLAGS" to "-O2",
|
||||
"LDFLAGS" to "-Wl,-z,max-page-size=16384"
|
||||
)
|
||||
run(listOf("../musl-$version/configure", "--target=$triple"), cwd = buildDir, env = configureEnv)
|
||||
run(listOf("make", "-j$jobs", "LIBCC=$libccPath"), cwd = buildDir, env = configureEnv)
|
||||
val dest = File(out, "$abi/libmusl-linker.so")
|
||||
dest.parentFile.mkdirs()
|
||||
run(listOf(strip, File(buildDir, "lib/libc.so").absolutePath, "-o", dest.absolutePath))
|
||||
dest.setExecutable(true, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val muslBridgeTask = tasks.register<BuildMuslBridgeTask>("buildMuslBridge") {
|
||||
group = "build"
|
||||
description = "Builds the patched musl dynamic linker (libmusl-linker.so) with the W^X exec bridge into generated jniLibs."
|
||||
muslVersion.set("1.2.5")
|
||||
patchFile.set(rootProject.file("scripts/patches/musl-1.2.5-dkc-exec-bridge.patch"))
|
||||
x80ShimFile.set(rootProject.file("scripts/musl-bridge/dkc_mulxc3.c"))
|
||||
val hostTag = if (System.getProperty("os.name").lowercase().contains("mac")) "darwin-x86_64" else "linux-x86_64"
|
||||
ndkToolchainDir.set(File(android.ndkDirectory, "toolchains/llvm/prebuilt/$hostTag"))
|
||||
workDir.set(layout.buildDirectory.dir("musl-bridge"))
|
||||
outputDir.set(layout.buildDirectory.dir("generated/jniLibs/muslBridge"))
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants { variant ->
|
||||
val capName = variant.name.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
|
||||
val variantBuildTypeName = variant.buildType ?: "release"
|
||||
val cxxBuildType = if (variantBuildTypeName.equals("debug", ignoreCase = true)) "Debug" else "RelWithDebInfo"
|
||||
val nativeBuildTaskName = "buildCMake$cxxBuildType"
|
||||
|
||||
tasks.matching { it.name == "merge${capName}NativeLibs" }.configureEach {
|
||||
dependsOn(muslBridgeTask)
|
||||
}
|
||||
variant.sources.jniLibs?.addGeneratedSourceDirectory(
|
||||
muslBridgeTask,
|
||||
BuildMuslBridgeTask::outputDir
|
||||
)
|
||||
val syncNodeLauncherTask = tasks.register<SyncNativeExecutableJniLibsTask>("syncNodeLauncherJniLibs$capName") {
|
||||
group = "build"
|
||||
description = "Copies ABI-specific node launcher executables into generated jniLibs for ${variant.name}."
|
||||
variantName.set(variant.name)
|
||||
buildTypeName.set(variantBuildTypeName)
|
||||
executableName.set("node_launcher")
|
||||
packagedLibraryName.set("libnode_launcher.so")
|
||||
cxxRoot.set(layout.buildDirectory.dir("intermediates/cxx/$cxxBuildType"))
|
||||
outputDir.set(layout.buildDirectory.dir("generated/jniLibs/nodeLauncher/${variant.name}"))
|
||||
dependsOn(nativeBuildTaskName)
|
||||
}
|
||||
|
||||
val syncGoLoaderTask = tasks.register<SyncNativeExecutableJniLibsTask>("syncGoExecLoaderJniLibs$capName") {
|
||||
group = "build"
|
||||
description = "Copies ABI-specific Go exec loader executables into generated jniLibs for ${variant.name}."
|
||||
variantName.set(variant.name)
|
||||
buildTypeName.set(variantBuildTypeName)
|
||||
executableName.set("go_exec_loader")
|
||||
packagedLibraryName.set("libgo_exec_loader.so")
|
||||
cxxRoot.set(layout.buildDirectory.dir("intermediates/cxx/$cxxBuildType"))
|
||||
outputDir.set(layout.buildDirectory.dir("generated/jniLibs/goExecLoader/${variant.name}"))
|
||||
dependsOn(nativeBuildTaskName)
|
||||
}
|
||||
|
||||
tasks.matching { it.name == "merge${capName}NativeLibs" }.configureEach {
|
||||
dependsOn(syncNodeLauncherTask)
|
||||
dependsOn(syncGoLoaderTask)
|
||||
}
|
||||
|
||||
variant.sources.jniLibs?.addGeneratedSourceDirectory(
|
||||
syncNodeLauncherTask,
|
||||
SyncNativeExecutableJniLibsTask::outputDir
|
||||
)
|
||||
variant.sources.jniLibs?.addGeneratedSourceDirectory(
|
||||
syncGoLoaderTask,
|
||||
SyncNativeExecutableJniLibsTask::outputDir
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = "com.google.protobuf:protoc:3.25.5"
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().forEach { task ->
|
||||
task.builtins {
|
||||
|
||||
create("java") {
|
||||
option("lite")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("androidx.documentfile:documentfile:1.0.1")
|
||||
|
||||
implementation("com.google.protobuf:protobuf-javalite:3.25.5")
|
||||
|
||||
implementation("com.google.android.material:material:1.10.0")
|
||||
implementation("androidx.activity:activity-compose:1.8.1")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.2")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
|
||||
|
||||
implementation(platform("androidx.compose:compose-bom:2026.06.01"))
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.material3:material3:1.5.0-alpha17")
|
||||
|
||||
implementation("androidx.compose.material:material-icons-extended:1.7.8")
|
||||
implementation("androidx.navigation:navigation-compose:2.7.5")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
|
||||
implementation("androidx.room:room-runtime:2.7.2")
|
||||
implementation("androidx.room:room-ktx:2.7.2")
|
||||
ksp("androidx.room:room-compiler:2.7.2")
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
||||
|
||||
implementation("io.coil-kt:coil-compose:2.5.0")
|
||||
implementation("io.coil-kt:coil-video:2.5.0")
|
||||
|
||||
implementation("com.google.code.gson:gson:2.10.1")
|
||||
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0")
|
||||
|
||||
implementation(platform("com.google.firebase:firebase-bom:33.7.0"))
|
||||
implementation("com.google.firebase:firebase-messaging")
|
||||
|
||||
implementation("org.bouncycastle:bcpkix-jdk15to18:1.78.1")
|
||||
implementation("org.bouncycastle:bcprov-jdk15to18:1.78.1")
|
||||
|
||||
implementation("io.insert-koin:koin-android:3.5.3")
|
||||
implementation("io.insert-koin:koin-androidx-compose:3.5.3")
|
||||
|
||||
implementation("androidx.webkit:webkit:1.9.0")
|
||||
|
||||
implementation("androidx.datastore:datastore-preferences:1.0.0")
|
||||
|
||||
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
||||
|
||||
implementation("org.apache.commons:commons-compress:1.26.0")
|
||||
implementation("org.tukaani:xz:1.9")
|
||||
|
||||
implementation("com.android.tools.build:apksig:8.3.0")
|
||||
|
||||
implementation("org.mozilla.geckoview:geckoview-arm64-v8a:142.0.20250827004350")
|
||||
|
||||
// Forced HTTP/3 (issue #721): Chromium's own network stack as the MITM bridge's
|
||||
// upstream leg. Java classes ship in the APK; libcronet is NEVER bundled — the host
|
||||
// downloads it on demand (filesDir/cronet_deps) and ApkBuilder injects it into
|
||||
// exported APKs only when the app enables 强制 HTTP/3 (libnode/GeckoView precedent).
|
||||
// Version must match CronetDependencyManager.CRONET_ARTIFACT_VERSION.
|
||||
implementation("org.chromium.net:cronet-embedded:143.7445.0")
|
||||
|
||||
implementation("com.google.zxing:core:3.5.2")
|
||||
|
||||
// Native Google sign-in through the Jetpack Credential Manager (NativeBridge).
|
||||
implementation("androidx.credentials:credentials:1.5.0")
|
||||
implementation("androidx.credentials:credentials-play-services-auth:1.5.0")
|
||||
implementation("com.google.android.libraries.identity.googleid:googleid:1.1.1")
|
||||
implementation("androidx.browser:browser:1.8.0")
|
||||
|
||||
implementation("androidx.media:media:1.7.0")
|
||||
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
|
||||
testImplementation("com.google.truth:truth:1.1.5")
|
||||
testImplementation("org.robolectric:robolectric:4.12.2")
|
||||
testImplementation("androidx.test:core:1.5.0")
|
||||
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a Python 3 interpreter command for the current platform. Windows usually exposes
|
||||
* Python as `python` (or the `py -3` launcher) rather than `python3`, so a hardcoded
|
||||
* `python3` fails there. Each candidate is verified to actually be Python 3 before use.
|
||||
* NOTE: intentionally duplicated in the root build.gradle.kts (this project has no buildSrc).
|
||||
*/
|
||||
fun resolvePython3Command(): List<String> {
|
||||
val isWindows = System.getProperty("os.name").lowercase().contains("windows")
|
||||
val candidates: List<List<String>> = if (isWindows) {
|
||||
listOf(listOf("python3"), listOf("python"), listOf("py", "-3"))
|
||||
} else {
|
||||
listOf(listOf("python3"), listOf("python"))
|
||||
}
|
||||
for (candidate in candidates) {
|
||||
try {
|
||||
val probe = ProcessBuilder(candidate + "--version").redirectErrorStream(true).start()
|
||||
val output = probe.inputStream.bufferedReader().readText()
|
||||
if (probe.waitFor() == 0 && output.contains("Python 3")) {
|
||||
return candidate
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Candidate unavailable; try the next one.
|
||||
}
|
||||
}
|
||||
return listOf("python3")
|
||||
}
|
||||
|
||||
tasks.register("checkConfigFieldDrift") {
|
||||
group = "verification"
|
||||
description = "Detect field-name drift between ApkConfig payload keys and ShellConfig @SerializedName (static guard for preview/export config consistency)"
|
||||
val script = rootProject.file("scripts/check_config_field_drift.py")
|
||||
val payloadFile = file("src/main/java/br/com/drukstech/codeapp/core/apkbuilder/ApkConfigJsonFactory.kt")
|
||||
val apkConfigFile = file("src/main/java/br/com/drukstech/codeapp/core/apkbuilder/ApkConfig.kt")
|
||||
val shellConfigFile = file("src/main/java/br/com/drukstech/codeapp/core/shell/ShellModeManager.kt")
|
||||
val allowlist = rootProject.file("scripts/config_field_drift_allowlist.json")
|
||||
// Configuration-cache safe: capture values at configuration time; the doLast action
|
||||
// must not reach through Project.
|
||||
val rootDir = rootProject.projectDir
|
||||
inputs.files(script, payloadFile, apkConfigFile, shellConfigFile, allowlist)
|
||||
outputs.upToDateWhen { false }
|
||||
doLast {
|
||||
val pb = ProcessBuilder(resolvePython3Command() + script.absolutePath)
|
||||
pb.directory(rootDir)
|
||||
pb.redirectErrorStream(true)
|
||||
val proc = pb.start()
|
||||
val log = proc.inputStream.bufferedReader().readText()
|
||||
val code = proc.waitFor()
|
||||
logger.lifecycle(log.trim())
|
||||
if (code != 0) {
|
||||
throw GradleException("checkConfigFieldDrift failed ($code)")
|
||||
}
|
||||
}
|
||||
}
|
||||
315
app/proguard-rules.pro
vendored
Normal file
315
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,315 @@
|
||||
# DruksCode ProGuard Rules
|
||||
#
|
||||
# 策略:启用代码收缩(移除未使用代码)+ 资源压缩(移除未使用资源),
|
||||
# 禁用混淆与激进优化。这是经过线上事故反复验证后的"稳定优先"配置,
|
||||
# 减包体积仍能保留 30%+ 收益,但完全避免反射/泛型/ServiceLoader/JNI 类陷阱。
|
||||
#
|
||||
# 收益与权衡:
|
||||
# - shrink: 保留,移除未使用类/方法(约 25% 体积削减)
|
||||
# - obfuscate: 关闭(开源项目无意义,反而引入 Gson/Koin/反射 bug)
|
||||
# - optimize: 关闭部分激进优化(保留默认收缩,避免内联引发的 NPE)
|
||||
#
|
||||
# 出问题时排查方法:
|
||||
# 1. ./gradlew :app:assembleRelease -PandroidProguardPrintUsage=true
|
||||
# 会在 build/outputs/mapping/release/usage.txt 写出被 R8 删除的所有内容
|
||||
# 2. ./gradlew :app:assembleRelease -PandroidProguardPrintSeeds=true
|
||||
# 会写出被显式 keep 的所有内容
|
||||
# 3. 把崩溃栈对照 build/outputs/mapping/release/mapping.txt 反推
|
||||
|
||||
-dontobfuscate
|
||||
|
||||
# 关掉一组容易破坏反射 / Compose / 协程语义的优化模式
|
||||
# 这些是 R8 历史上反复出现 bug 的优化通道,关掉它们体积代价可忽略
|
||||
-optimizations !class/merging/*,!field/*,!method/marking/static,!method/inlining/*,!code/allocation/variable
|
||||
-optimizationpasses 1
|
||||
|
||||
# ============================================================
|
||||
# 调试 / 崩溃堆栈可读
|
||||
# ============================================================
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-keepattributes Signature
|
||||
-keepattributes Exceptions
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes EnclosingMethod
|
||||
-keepattributes InnerClasses
|
||||
-keepattributes RuntimeVisibleAnnotations,RuntimeVisibleParameterAnnotations
|
||||
-keepattributes RuntimeInvisibleAnnotations,RuntimeInvisibleParameterAnnotations
|
||||
-keepattributes RuntimeVisibleTypeAnnotations,RuntimeInvisibleTypeAnnotations
|
||||
-keepattributes AnnotationDefault
|
||||
-keepattributes MethodParameters
|
||||
|
||||
# 让崩溃栈打印 R8 重命名前的类名 / 行号
|
||||
-renamesourcefileattribute SourceFile
|
||||
|
||||
# ============================================================
|
||||
# Android 组件 — Manifest 引用,必须保名
|
||||
# ============================================================
|
||||
-keep public class * extends android.app.Activity
|
||||
-keep public class * extends android.app.Service
|
||||
-keep public class * extends android.content.BroadcastReceiver
|
||||
-keep public class * extends android.content.ContentProvider
|
||||
-keep public class * extends android.accessibilityservice.AccessibilityService
|
||||
-keep public class * extends androidx.work.ListenableWorker
|
||||
-keep public class * extends android.app.Application
|
||||
-keep public class * extends androidx.fragment.app.Fragment
|
||||
-keep public class * extends androidx.lifecycle.ViewModel
|
||||
-keep public class * extends androidx.lifecycle.AndroidViewModel
|
||||
|
||||
# ============================================================
|
||||
# 通用反射 — Parcelable / Serializable / enum / native
|
||||
# ============================================================
|
||||
-keepclassmembers enum * {
|
||||
public static **[] values();
|
||||
public static ** valueOf(java.lang.String);
|
||||
}
|
||||
|
||||
-keep class * implements android.os.Parcelable {
|
||||
public static final android.os.Parcelable$Creator *;
|
||||
}
|
||||
|
||||
-keepclassmembers class * implements java.io.Serializable {
|
||||
static final long serialVersionUID;
|
||||
private static java.io.ObjectStreamField[] serialPersistentFields;
|
||||
private void writeObject(java.io.ObjectOutputStream);
|
||||
private void readObject(java.io.ObjectInputStream);
|
||||
java.lang.Object writeReplace();
|
||||
java.lang.Object readResolve();
|
||||
}
|
||||
|
||||
# JNI native 方法
|
||||
-keepclasseswithmembernames class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
# WebView @JavascriptInterface
|
||||
-keepclassmembers class * {
|
||||
@android.webkit.JavascriptInterface <methods>;
|
||||
}
|
||||
|
||||
# View 的 XML inflate / setOnClick 反射回调
|
||||
-keepclassmembers class * extends android.view.View {
|
||||
void set*(***);
|
||||
*** get*();
|
||||
}
|
||||
-keepclassmembers class * extends android.app.Activity {
|
||||
public void *(android.view.View);
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Kotlin
|
||||
# ============================================================
|
||||
-keep class kotlin.Metadata { *; }
|
||||
-keep class kotlin.reflect.** { *; }
|
||||
-keepclassmembers class **$WhenMappings { <fields>; }
|
||||
-keepclassmembers class kotlin.Lazy { public <methods>; }
|
||||
-keep class kotlin.jvm.internal.DefaultConstructorMarker { *; }
|
||||
-keepclassmembers class kotlin.coroutines.jvm.internal.** { *; }
|
||||
-dontwarn kotlin.**
|
||||
-dontwarn kotlinx.**
|
||||
|
||||
# Kotlin Coroutines — ServiceLoader 加载 Dispatchers.Main
|
||||
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory
|
||||
-keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory
|
||||
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory { *; }
|
||||
-keep class kotlinx.coroutines.android.AndroidExceptionPreHandler { *; }
|
||||
-keep class kotlinx.coroutines.internal.MainDispatcherFactory { *; }
|
||||
-keep class kotlinx.coroutines.CoroutineExceptionHandler { *; }
|
||||
-keepclassmembers class kotlinx.coroutines.** {
|
||||
volatile <fields>;
|
||||
}
|
||||
# Coroutines 调试 / 异常 hooks
|
||||
-keepnames class kotlinx.coroutines.flow.** { *; }
|
||||
|
||||
# ============================================================
|
||||
# 项目自身代码 — 全部保留(开源 + 重反射)
|
||||
# ============================================================
|
||||
-keep class br.com.drukstech.codeapp.** { *; }
|
||||
-keepclassmembers class br.com.drukstech.codeapp.** { *; }
|
||||
-keep enum br.com.drukstech.codeapp.** { *; }
|
||||
-keep interface br.com.drukstech.codeapp.** { *; }
|
||||
|
||||
# data class 的合成构造器(含默认参数)— Gson 反序列化必须
|
||||
-keepclassmembers class br.com.drukstech.codeapp.data.model.** {
|
||||
<init>(...);
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Room — KSP 生成的 _Impl 类已经在 br.com.drukstech.codeapp.** 范围内
|
||||
# ============================================================
|
||||
-keep class * extends androidx.room.RoomDatabase { *; }
|
||||
-keep @androidx.room.Entity class *
|
||||
-keep @androidx.room.Dao class *
|
||||
-keepclassmembers class * {
|
||||
@androidx.room.* <methods>;
|
||||
}
|
||||
-dontwarn androidx.room.paging.**
|
||||
|
||||
# ============================================================
|
||||
# Gson — 序列化 / 反序列化通过反射
|
||||
# ============================================================
|
||||
-keep class sun.misc.Unsafe { *; }
|
||||
-keep class com.google.gson.** { *; }
|
||||
-keep class * implements com.google.gson.TypeAdapter
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
-keep class * implements com.google.gson.InstanceCreator
|
||||
-keepclassmembers class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
@com.google.gson.annotations.JsonAdapter <fields>;
|
||||
@com.google.gson.annotations.Expose <fields>;
|
||||
}
|
||||
|
||||
# Gson TypeToken — R8 full mode 会把 `object : TypeToken<List<...>>() {}`
|
||||
# 匿名子类的 Signature 属性剥掉,运行时抛 IllegalStateException。
|
||||
# 参考: https://github.com/google/gson/blob/main/Troubleshooting.md#r8
|
||||
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
|
||||
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
|
||||
|
||||
# ============================================================
|
||||
# Koin DI — 大量反射 + ServiceLoader
|
||||
# ============================================================
|
||||
-keep class org.koin.** { *; }
|
||||
-keepclassmembers class * {
|
||||
public <init>(org.koin.core.scope.Scope);
|
||||
}
|
||||
-keepclassmembers class * extends org.koin.core.module.Module { *; }
|
||||
-dontwarn org.koin.**
|
||||
|
||||
# ============================================================
|
||||
# OkHttp / Okio — Platform 反射检测 OS 安全栈
|
||||
# ============================================================
|
||||
-keep class okhttp3.internal.platform.** { *; }
|
||||
-keep class okhttp3.internal.publicsuffix.** { *; }
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
-dontwarn org.conscrypt.**
|
||||
-dontwarn org.bouncycastle.**
|
||||
-dontwarn org.openjsse.**
|
||||
|
||||
# ============================================================
|
||||
# Coil — ServiceLoader 加载 fetcher / decoder / mapper
|
||||
# ============================================================
|
||||
-keep class coil.util.** { *; }
|
||||
-keep class coil.fetch.** { *; }
|
||||
-keep class coil.decode.** { *; }
|
||||
-keep class coil.map.** { *; }
|
||||
-dontwarn coil.**
|
||||
|
||||
# ============================================================
|
||||
# apksig — 反射读取 @Asn1Class/@Asn1Field 注解序列化 PKCS#7
|
||||
# ============================================================
|
||||
-keep class com.android.apksig.** { *; }
|
||||
-keepclassmembers class com.android.apksig.** { *; }
|
||||
-keep @com.android.apksig.internal.asn1.Asn1Class class *
|
||||
-keepclassmembers class * {
|
||||
@com.android.apksig.internal.asn1.Asn1Field *;
|
||||
}
|
||||
-dontwarn com.android.apksig.**
|
||||
|
||||
-keep class com.android.aapt.** { *; }
|
||||
-keepclassmembers class com.android.aapt.** { *; }
|
||||
-keep class com.android.bundle.** { *; }
|
||||
-keepclassmembers class com.android.bundle.** { *; }
|
||||
|
||||
# ============================================================
|
||||
# GeckoView — 大量 JNI / 注解反射
|
||||
# ============================================================
|
||||
-keep class org.mozilla.geckoview.** { *; }
|
||||
-keep class org.mozilla.gecko.** { *; }
|
||||
-keepclassmembers class * extends org.mozilla.geckoview.GeckoSession$* { *; }
|
||||
-dontwarn org.mozilla.geckoview.**
|
||||
|
||||
# ============================================================
|
||||
# ZXing — 反射查找编码格式 (仅 core,embedded 已移除)
|
||||
# ============================================================
|
||||
-keep class com.google.zxing.** { *; }
|
||||
-dontwarn com.google.zxing.**
|
||||
|
||||
# BillingClient — AIDL stub
|
||||
# ============================================================
|
||||
-keep class com.android.vending.billing.** { *; }
|
||||
-keep class com.android.billingclient.** { *; }
|
||||
-keep class com.google.android.gms.internal.** { *; }
|
||||
-dontwarn com.android.billingclient.**
|
||||
|
||||
# ============================================================
|
||||
# Credentials API + GoogleId — 反射解析 ID Token
|
||||
# ============================================================
|
||||
-keep class androidx.credentials.** { *; }
|
||||
-keep class com.google.android.libraries.identity.** { *; }
|
||||
-keep class com.google.android.gms.auth.api.identity.** { *; }
|
||||
-dontwarn androidx.credentials.**
|
||||
-dontwarn com.google.android.libraries.identity.**
|
||||
|
||||
# ============================================================
|
||||
# DataStore Preferences
|
||||
# ============================================================
|
||||
-keep class androidx.datastore.** { *; }
|
||||
-dontwarn androidx.datastore.**
|
||||
|
||||
# ============================================================
|
||||
# Security crypto (alpha — 可能有反射)
|
||||
# ============================================================
|
||||
-keep class androidx.security.crypto.** { *; }
|
||||
-keep class com.google.crypto.tink.** { *; }
|
||||
-dontwarn androidx.security.crypto.**
|
||||
-dontwarn com.google.crypto.tink.**
|
||||
|
||||
# ============================================================
|
||||
# Compress / xz — ServiceLoader 加载格式
|
||||
# ============================================================
|
||||
-keep class org.apache.commons.compress.compressors.FileNameUtil { *; }
|
||||
-keep class org.apache.commons.compress.archivers.** { *; }
|
||||
-keep class org.tukaani.xz.** { *; }
|
||||
-dontwarn org.apache.commons.compress.**
|
||||
-dontwarn org.tukaani.xz.**
|
||||
-dontwarn org.brotli.dec.**
|
||||
-dontwarn org.yaml.snakeyaml.**
|
||||
|
||||
# ============================================================
|
||||
# Compose Runtime — 已有 consumer rules,仅压制 warn
|
||||
# ============================================================
|
||||
-dontwarn androidx.compose.**
|
||||
|
||||
# ============================================================
|
||||
# Material / AppCompat — 已有 consumer rules,仅兜底
|
||||
# ============================================================
|
||||
-dontwarn com.google.android.material.**
|
||||
|
||||
# ============================================================
|
||||
# Custom Tabs (browser)
|
||||
# ============================================================
|
||||
-keep class androidx.browser.** { *; }
|
||||
-dontwarn androidx.browser.**
|
||||
|
||||
# ============================================================
|
||||
# Haze (背景模糊)
|
||||
# ============================================================
|
||||
-keep class dev.chrisbanes.haze.** { *; }
|
||||
-dontwarn dev.chrisbanes.haze.**
|
||||
|
||||
# Node.js JNI output bridge (R8 may rename onOutput otherwise)
|
||||
-keep class br.com.drukstech.codeapp.core.nodejs.NodeBridge { *; }
|
||||
-keep class br.com.drukstech.codeapp.core.nodejs.NodeJniOutputBridge {
|
||||
<init>(...);
|
||||
public void onOutput(java.lang.String, boolean);
|
||||
}
|
||||
|
||||
# Firebase / FCM
|
||||
-keep class com.google.firebase.** { *; }
|
||||
-keep class com.google.android.gms.** { *; }
|
||||
-dontwarn com.google.firebase.**
|
||||
-dontwarn com.google.android.gms.**
|
||||
|
||||
# ============================================================
|
||||
# Cronet (forced HTTP/3 upstream)
|
||||
# ============================================================
|
||||
# Cronet's native code reaches its Java classes via JNI and ServiceLoader
|
||||
# (org.chromium.net.impl.** / org.chromium.base.**); R8 must not rename or
|
||||
# strip them. The AAR ships consumer rules, this is the belt-and-braces copy.
|
||||
-keep class org.chromium.net.** { *; }
|
||||
-keep class org.chromium.base.** { *; }
|
||||
-keep class org.chromium.components.** { *; }
|
||||
-dontwarn org.chromium.**
|
||||
-keepdirectories META-INF/services/**
|
||||
@ -0,0 +1,563 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 45,
|
||||
"identityHash": "ea6b40911e9117204ec107ff693031d7",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "web_apps",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `iconPath` TEXT, `packageName` TEXT, `appType` TEXT NOT NULL, `mediaConfig` TEXT, `galleryConfig` TEXT, `htmlConfig` TEXT, `wordpressConfig` TEXT, `nodejsConfig` TEXT, `phpAppConfig` TEXT, `pythonAppConfig` TEXT, `goAppConfig` TEXT, `multiWebConfig` TEXT, `activationEnabled` INTEGER NOT NULL, `activationCodeList` TEXT NOT NULL, `activationRequireEveryTime` INTEGER NOT NULL, `activationRemoteConfig` TEXT, `isActivated` INTEGER NOT NULL, `adsEnabled` INTEGER NOT NULL, `adConfig` TEXT, `announcementEnabled` INTEGER NOT NULL, `announcement` TEXT, `adBlockEnabled` INTEGER NOT NULL, `adBlockRules` TEXT NOT NULL, `adBlockSubscriptions` TEXT NOT NULL, `webViewConfig` TEXT NOT NULL, `splashEnabled` INTEGER NOT NULL, `splashConfig` TEXT, `bgmEnabled` INTEGER NOT NULL, `bgmConfig` TEXT, `apkExportConfig` TEXT, `themeType` TEXT NOT NULL, `translateEnabled` INTEGER NOT NULL, `translateConfig` TEXT, `extensionEnabled` INTEGER NOT NULL, `extensionModuleIds` TEXT NOT NULL, `extensionFabIcon` TEXT, `autoStartConfig` TEXT, `browserDisguiseConfig` TEXT, `deviceDisguiseConfig` TEXT, `activationDialogConfig` TEXT, `categoryId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconPath",
|
||||
"columnName": "iconPath",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "packageName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "appType",
|
||||
"columnName": "appType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mediaConfig",
|
||||
"columnName": "mediaConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "galleryConfig",
|
||||
"columnName": "galleryConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "htmlConfig",
|
||||
"columnName": "htmlConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "wordpressConfig",
|
||||
"columnName": "wordpressConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "nodejsConfig",
|
||||
"columnName": "nodejsConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "phpAppConfig",
|
||||
"columnName": "phpAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pythonAppConfig",
|
||||
"columnName": "pythonAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "goAppConfig",
|
||||
"columnName": "goAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "multiWebConfig",
|
||||
"columnName": "multiWebConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationEnabled",
|
||||
"columnName": "activationEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationCodeList",
|
||||
"columnName": "activationCodeList",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRequireEveryTime",
|
||||
"columnName": "activationRequireEveryTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRemoteConfig",
|
||||
"columnName": "activationRemoteConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActivated",
|
||||
"columnName": "isActivated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adsEnabled",
|
||||
"columnName": "adsEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adConfig",
|
||||
"columnName": "adConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcementEnabled",
|
||||
"columnName": "announcementEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcement",
|
||||
"columnName": "announcement",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockEnabled",
|
||||
"columnName": "adBlockEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockRules",
|
||||
"columnName": "adBlockRules",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockSubscriptions",
|
||||
"columnName": "adBlockSubscriptions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "webViewConfig",
|
||||
"columnName": "webViewConfig",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashEnabled",
|
||||
"columnName": "splashEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashConfig",
|
||||
"columnName": "splashConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmEnabled",
|
||||
"columnName": "bgmEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmConfig",
|
||||
"columnName": "bgmConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "apkExportConfig",
|
||||
"columnName": "apkExportConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "themeType",
|
||||
"columnName": "themeType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateEnabled",
|
||||
"columnName": "translateEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateConfig",
|
||||
"columnName": "translateConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionEnabled",
|
||||
"columnName": "extensionEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionModuleIds",
|
||||
"columnName": "extensionModuleIds",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionFabIcon",
|
||||
"columnName": "extensionFabIcon",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "autoStartConfig",
|
||||
"columnName": "autoStartConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "browserDisguiseConfig",
|
||||
"columnName": "browserDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceDisguiseConfig",
|
||||
"columnName": "deviceDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationDialogConfig",
|
||||
"columnName": "activationDialogConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryId",
|
||||
"columnName": "categoryId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_web_apps_updatedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"updatedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_updatedAt` ON `${TABLE_NAME}` (`updatedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_categoryId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"categoryId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_categoryId` ON `${TABLE_NAME}` (`categoryId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_isActivated",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"isActivated"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_isActivated` ON `${TABLE_NAME}` (`isActivated`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_url` ON `${TABLE_NAME}` (`appType`, `url`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_iconPath_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"iconPath",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_iconPath_url` ON `${TABLE_NAME}` (`appType`, `iconPath`, `url`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_categories",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT NOT NULL, `color` TEXT NOT NULL, `sortOrder` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sortOrder",
|
||||
"columnName": "sortOrder",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "app_usage_stats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `launchCount` INTEGER NOT NULL, `totalUsageMs` INTEGER NOT NULL, `lastUsedAt` INTEGER NOT NULL, `lastSessionDurationMs` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "launchCount",
|
||||
"columnName": "launchCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "totalUsageMs",
|
||||
"columnName": "totalUsageMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUsedAt",
|
||||
"columnName": "lastUsedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSessionDurationMs",
|
||||
"columnName": "lastSessionDurationMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_usage_stats_appId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_app_usage_stats_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_lastUsedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"lastUsedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_lastUsedAt` ON `${TABLE_NAME}` (`lastUsedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_launchCount",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"launchCount"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_launchCount` ON `${TABLE_NAME}` (`launchCount`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_health_records",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `url` TEXT NOT NULL, `status` TEXT NOT NULL, `responseTimeMs` INTEGER NOT NULL, `httpStatusCode` INTEGER NOT NULL, `errorMessage` TEXT, `checkedAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "responseTimeMs",
|
||||
"columnName": "responseTimeMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpStatusCode",
|
||||
"columnName": "httpStatusCode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "errorMessage",
|
||||
"columnName": "errorMessage",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "checkedAt",
|
||||
"columnName": "checkedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_health_records_appId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_health_records_checkedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"checkedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_checkedAt` ON `${TABLE_NAME}` (`checkedAt`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ea6b40911e9117204ec107ff693031d7')"
|
||||
]
|
||||
}
|
||||
}
|
||||
573
app/schemas/com.webtoapp.data.database.AppDatabase/36.json
Normal file
573
app/schemas/com.webtoapp.data.database.AppDatabase/36.json
Normal file
@ -0,0 +1,573 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 36,
|
||||
"identityHash": "edf2cccde4bb442d6b044c0811e03272",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "web_apps",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `iconPath` TEXT, `packageName` TEXT, `appType` TEXT NOT NULL, `mediaConfig` TEXT, `galleryConfig` TEXT, `htmlConfig` TEXT, `wordpressConfig` TEXT, `nodejsConfig` TEXT, `phpAppConfig` TEXT, `pythonAppConfig` TEXT, `goAppConfig` TEXT, `multiWebConfig` TEXT, `activationEnabled` INTEGER NOT NULL, `activationCodes` TEXT NOT NULL, `activationCodeList` TEXT NOT NULL, `activationRequireEveryTime` INTEGER NOT NULL, `isActivated` INTEGER NOT NULL, `adsEnabled` INTEGER NOT NULL, `adConfig` TEXT, `announcementEnabled` INTEGER NOT NULL, `announcement` TEXT, `adBlockEnabled` INTEGER NOT NULL, `adBlockRules` TEXT NOT NULL, `webViewConfig` TEXT NOT NULL, `splashEnabled` INTEGER NOT NULL, `splashConfig` TEXT, `bgmEnabled` INTEGER NOT NULL, `bgmConfig` TEXT, `apkExportConfig` TEXT, `themeType` TEXT NOT NULL, `translateEnabled` INTEGER NOT NULL, `translateConfig` TEXT, `extensionEnabled` INTEGER NOT NULL, `extensionModuleIds` TEXT NOT NULL, `extensionFabIcon` TEXT, `autoStartConfig` TEXT, `forcedRunConfig` TEXT, `blackTechConfig` TEXT, `disguiseConfig` TEXT, `browserDisguiseConfig` TEXT, `deviceDisguiseConfig` TEXT, `activationDialogConfig` TEXT, `categoryId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconPath",
|
||||
"columnName": "iconPath",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "packageName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "appType",
|
||||
"columnName": "appType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mediaConfig",
|
||||
"columnName": "mediaConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "galleryConfig",
|
||||
"columnName": "galleryConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "htmlConfig",
|
||||
"columnName": "htmlConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "wordpressConfig",
|
||||
"columnName": "wordpressConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "nodejsConfig",
|
||||
"columnName": "nodejsConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "phpAppConfig",
|
||||
"columnName": "phpAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pythonAppConfig",
|
||||
"columnName": "pythonAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "goAppConfig",
|
||||
"columnName": "goAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "multiWebConfig",
|
||||
"columnName": "multiWebConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationEnabled",
|
||||
"columnName": "activationEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationCodes",
|
||||
"columnName": "activationCodes",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationCodeList",
|
||||
"columnName": "activationCodeList",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRequireEveryTime",
|
||||
"columnName": "activationRequireEveryTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActivated",
|
||||
"columnName": "isActivated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adsEnabled",
|
||||
"columnName": "adsEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adConfig",
|
||||
"columnName": "adConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcementEnabled",
|
||||
"columnName": "announcementEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcement",
|
||||
"columnName": "announcement",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockEnabled",
|
||||
"columnName": "adBlockEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockRules",
|
||||
"columnName": "adBlockRules",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "webViewConfig",
|
||||
"columnName": "webViewConfig",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashEnabled",
|
||||
"columnName": "splashEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashConfig",
|
||||
"columnName": "splashConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmEnabled",
|
||||
"columnName": "bgmEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmConfig",
|
||||
"columnName": "bgmConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "apkExportConfig",
|
||||
"columnName": "apkExportConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "themeType",
|
||||
"columnName": "themeType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateEnabled",
|
||||
"columnName": "translateEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateConfig",
|
||||
"columnName": "translateConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionEnabled",
|
||||
"columnName": "extensionEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionModuleIds",
|
||||
"columnName": "extensionModuleIds",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionFabIcon",
|
||||
"columnName": "extensionFabIcon",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "autoStartConfig",
|
||||
"columnName": "autoStartConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "forcedRunConfig",
|
||||
"columnName": "forcedRunConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "blackTechConfig",
|
||||
"columnName": "blackTechConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "disguiseConfig",
|
||||
"columnName": "disguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "browserDisguiseConfig",
|
||||
"columnName": "browserDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceDisguiseConfig",
|
||||
"columnName": "deviceDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationDialogConfig",
|
||||
"columnName": "activationDialogConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryId",
|
||||
"columnName": "categoryId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_web_apps_updatedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"updatedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_updatedAt` ON `${TABLE_NAME}` (`updatedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_categoryId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"categoryId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_categoryId` ON `${TABLE_NAME}` (`categoryId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_isActivated",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"isActivated"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_isActivated` ON `${TABLE_NAME}` (`isActivated`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_url` ON `${TABLE_NAME}` (`appType`, `url`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_iconPath_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"iconPath",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_iconPath_url` ON `${TABLE_NAME}` (`appType`, `iconPath`, `url`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_categories",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT NOT NULL, `color` TEXT NOT NULL, `sortOrder` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sortOrder",
|
||||
"columnName": "sortOrder",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "app_usage_stats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `launchCount` INTEGER NOT NULL, `totalUsageMs` INTEGER NOT NULL, `lastUsedAt` INTEGER NOT NULL, `lastSessionDurationMs` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "launchCount",
|
||||
"columnName": "launchCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "totalUsageMs",
|
||||
"columnName": "totalUsageMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUsedAt",
|
||||
"columnName": "lastUsedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSessionDurationMs",
|
||||
"columnName": "lastSessionDurationMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_usage_stats_appId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_app_usage_stats_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_lastUsedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"lastUsedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_lastUsedAt` ON `${TABLE_NAME}` (`lastUsedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_launchCount",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"launchCount"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_launchCount` ON `${TABLE_NAME}` (`launchCount`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_health_records",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `url` TEXT NOT NULL, `status` TEXT NOT NULL, `responseTimeMs` INTEGER NOT NULL, `httpStatusCode` INTEGER NOT NULL, `errorMessage` TEXT, `checkedAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "responseTimeMs",
|
||||
"columnName": "responseTimeMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpStatusCode",
|
||||
"columnName": "httpStatusCode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "errorMessage",
|
||||
"columnName": "errorMessage",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "checkedAt",
|
||||
"columnName": "checkedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_health_records_appId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_health_records_checkedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"checkedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_checkedAt` ON `${TABLE_NAME}` (`checkedAt`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'edf2cccde4bb442d6b044c0811e03272')"
|
||||
]
|
||||
}
|
||||
}
|
||||
567
app/schemas/com.webtoapp.data.database.AppDatabase/37.json
Normal file
567
app/schemas/com.webtoapp.data.database.AppDatabase/37.json
Normal file
@ -0,0 +1,567 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 37,
|
||||
"identityHash": "643c6f9480593253e00702649c9f1737",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "web_apps",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `iconPath` TEXT, `packageName` TEXT, `appType` TEXT NOT NULL, `mediaConfig` TEXT, `galleryConfig` TEXT, `htmlConfig` TEXT, `wordpressConfig` TEXT, `nodejsConfig` TEXT, `phpAppConfig` TEXT, `pythonAppConfig` TEXT, `goAppConfig` TEXT, `multiWebConfig` TEXT, `activationEnabled` INTEGER NOT NULL, `activationCodeList` TEXT NOT NULL, `activationRequireEveryTime` INTEGER NOT NULL, `isActivated` INTEGER NOT NULL, `adsEnabled` INTEGER NOT NULL, `adConfig` TEXT, `announcementEnabled` INTEGER NOT NULL, `announcement` TEXT, `adBlockEnabled` INTEGER NOT NULL, `adBlockRules` TEXT NOT NULL, `webViewConfig` TEXT NOT NULL, `splashEnabled` INTEGER NOT NULL, `splashConfig` TEXT, `bgmEnabled` INTEGER NOT NULL, `bgmConfig` TEXT, `apkExportConfig` TEXT, `themeType` TEXT NOT NULL, `translateEnabled` INTEGER NOT NULL, `translateConfig` TEXT, `extensionEnabled` INTEGER NOT NULL, `extensionModuleIds` TEXT NOT NULL, `extensionFabIcon` TEXT, `autoStartConfig` TEXT, `forcedRunConfig` TEXT, `blackTechConfig` TEXT, `disguiseConfig` TEXT, `browserDisguiseConfig` TEXT, `deviceDisguiseConfig` TEXT, `activationDialogConfig` TEXT, `categoryId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconPath",
|
||||
"columnName": "iconPath",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "packageName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "appType",
|
||||
"columnName": "appType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mediaConfig",
|
||||
"columnName": "mediaConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "galleryConfig",
|
||||
"columnName": "galleryConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "htmlConfig",
|
||||
"columnName": "htmlConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "wordpressConfig",
|
||||
"columnName": "wordpressConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "nodejsConfig",
|
||||
"columnName": "nodejsConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "phpAppConfig",
|
||||
"columnName": "phpAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pythonAppConfig",
|
||||
"columnName": "pythonAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "goAppConfig",
|
||||
"columnName": "goAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "multiWebConfig",
|
||||
"columnName": "multiWebConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationEnabled",
|
||||
"columnName": "activationEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationCodeList",
|
||||
"columnName": "activationCodeList",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRequireEveryTime",
|
||||
"columnName": "activationRequireEveryTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActivated",
|
||||
"columnName": "isActivated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adsEnabled",
|
||||
"columnName": "adsEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adConfig",
|
||||
"columnName": "adConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcementEnabled",
|
||||
"columnName": "announcementEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcement",
|
||||
"columnName": "announcement",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockEnabled",
|
||||
"columnName": "adBlockEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockRules",
|
||||
"columnName": "adBlockRules",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "webViewConfig",
|
||||
"columnName": "webViewConfig",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashEnabled",
|
||||
"columnName": "splashEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashConfig",
|
||||
"columnName": "splashConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmEnabled",
|
||||
"columnName": "bgmEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmConfig",
|
||||
"columnName": "bgmConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "apkExportConfig",
|
||||
"columnName": "apkExportConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "themeType",
|
||||
"columnName": "themeType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateEnabled",
|
||||
"columnName": "translateEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateConfig",
|
||||
"columnName": "translateConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionEnabled",
|
||||
"columnName": "extensionEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionModuleIds",
|
||||
"columnName": "extensionModuleIds",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionFabIcon",
|
||||
"columnName": "extensionFabIcon",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "autoStartConfig",
|
||||
"columnName": "autoStartConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "forcedRunConfig",
|
||||
"columnName": "forcedRunConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "blackTechConfig",
|
||||
"columnName": "blackTechConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "disguiseConfig",
|
||||
"columnName": "disguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "browserDisguiseConfig",
|
||||
"columnName": "browserDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceDisguiseConfig",
|
||||
"columnName": "deviceDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationDialogConfig",
|
||||
"columnName": "activationDialogConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryId",
|
||||
"columnName": "categoryId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_web_apps_updatedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"updatedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_updatedAt` ON `${TABLE_NAME}` (`updatedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_categoryId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"categoryId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_categoryId` ON `${TABLE_NAME}` (`categoryId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_isActivated",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"isActivated"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_isActivated` ON `${TABLE_NAME}` (`isActivated`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_url` ON `${TABLE_NAME}` (`appType`, `url`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_iconPath_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"iconPath",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_iconPath_url` ON `${TABLE_NAME}` (`appType`, `iconPath`, `url`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_categories",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT NOT NULL, `color` TEXT NOT NULL, `sortOrder` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sortOrder",
|
||||
"columnName": "sortOrder",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "app_usage_stats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `launchCount` INTEGER NOT NULL, `totalUsageMs` INTEGER NOT NULL, `lastUsedAt` INTEGER NOT NULL, `lastSessionDurationMs` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "launchCount",
|
||||
"columnName": "launchCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "totalUsageMs",
|
||||
"columnName": "totalUsageMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUsedAt",
|
||||
"columnName": "lastUsedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSessionDurationMs",
|
||||
"columnName": "lastSessionDurationMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_usage_stats_appId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_app_usage_stats_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_lastUsedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"lastUsedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_lastUsedAt` ON `${TABLE_NAME}` (`lastUsedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_launchCount",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"launchCount"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_launchCount` ON `${TABLE_NAME}` (`launchCount`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_health_records",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `url` TEXT NOT NULL, `status` TEXT NOT NULL, `responseTimeMs` INTEGER NOT NULL, `httpStatusCode` INTEGER NOT NULL, `errorMessage` TEXT, `checkedAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "responseTimeMs",
|
||||
"columnName": "responseTimeMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpStatusCode",
|
||||
"columnName": "httpStatusCode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "errorMessage",
|
||||
"columnName": "errorMessage",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "checkedAt",
|
||||
"columnName": "checkedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_health_records_appId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_health_records_checkedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"checkedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_checkedAt` ON `${TABLE_NAME}` (`checkedAt`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '643c6f9480593253e00702649c9f1737')"
|
||||
]
|
||||
}
|
||||
}
|
||||
578
app/schemas/com.webtoapp.data.database.AppDatabase/38.json
Normal file
578
app/schemas/com.webtoapp.data.database.AppDatabase/38.json
Normal file
@ -0,0 +1,578 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 38,
|
||||
"identityHash": "983cf11423592851e73dc30dd6bb36e3",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "web_apps",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `iconPath` TEXT, `packageName` TEXT, `appType` TEXT NOT NULL, `mediaConfig` TEXT, `galleryConfig` TEXT, `htmlConfig` TEXT, `wordpressConfig` TEXT, `nodejsConfig` TEXT, `phpAppConfig` TEXT, `pythonAppConfig` TEXT, `goAppConfig` TEXT, `multiWebConfig` TEXT, `activationEnabled` INTEGER NOT NULL, `activationCodeList` TEXT NOT NULL, `activationRequireEveryTime` INTEGER NOT NULL, `activationRemoteConfig` TEXT, `isActivated` INTEGER NOT NULL, `adsEnabled` INTEGER NOT NULL, `adConfig` TEXT, `announcementEnabled` INTEGER NOT NULL, `announcement` TEXT, `adBlockEnabled` INTEGER NOT NULL, `adBlockRules` TEXT NOT NULL, `adBlockSubscriptions` TEXT NOT NULL, `webViewConfig` TEXT NOT NULL, `splashEnabled` INTEGER NOT NULL, `splashConfig` TEXT, `bgmEnabled` INTEGER NOT NULL, `bgmConfig` TEXT, `apkExportConfig` TEXT, `themeType` TEXT NOT NULL, `translateEnabled` INTEGER NOT NULL, `translateConfig` TEXT, `extensionEnabled` INTEGER NOT NULL, `extensionModuleIds` TEXT NOT NULL, `extensionFabIcon` TEXT, `autoStartConfig` TEXT, `forcedRunConfig` TEXT, `blackTechConfig` TEXT, `disguiseConfig` TEXT, `browserDisguiseConfig` TEXT, `deviceDisguiseConfig` TEXT, `activationDialogConfig` TEXT, `categoryId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconPath",
|
||||
"columnName": "iconPath",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "packageName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "appType",
|
||||
"columnName": "appType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mediaConfig",
|
||||
"columnName": "mediaConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "galleryConfig",
|
||||
"columnName": "galleryConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "htmlConfig",
|
||||
"columnName": "htmlConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "wordpressConfig",
|
||||
"columnName": "wordpressConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "nodejsConfig",
|
||||
"columnName": "nodejsConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "phpAppConfig",
|
||||
"columnName": "phpAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pythonAppConfig",
|
||||
"columnName": "pythonAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "goAppConfig",
|
||||
"columnName": "goAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "multiWebConfig",
|
||||
"columnName": "multiWebConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationEnabled",
|
||||
"columnName": "activationEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationCodeList",
|
||||
"columnName": "activationCodeList",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRequireEveryTime",
|
||||
"columnName": "activationRequireEveryTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRemoteConfig",
|
||||
"columnName": "activationRemoteConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActivated",
|
||||
"columnName": "isActivated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adsEnabled",
|
||||
"columnName": "adsEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adConfig",
|
||||
"columnName": "adConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcementEnabled",
|
||||
"columnName": "announcementEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcement",
|
||||
"columnName": "announcement",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockEnabled",
|
||||
"columnName": "adBlockEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockRules",
|
||||
"columnName": "adBlockRules",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockSubscriptions",
|
||||
"columnName": "adBlockSubscriptions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "webViewConfig",
|
||||
"columnName": "webViewConfig",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashEnabled",
|
||||
"columnName": "splashEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashConfig",
|
||||
"columnName": "splashConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmEnabled",
|
||||
"columnName": "bgmEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmConfig",
|
||||
"columnName": "bgmConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "apkExportConfig",
|
||||
"columnName": "apkExportConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "themeType",
|
||||
"columnName": "themeType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateEnabled",
|
||||
"columnName": "translateEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateConfig",
|
||||
"columnName": "translateConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionEnabled",
|
||||
"columnName": "extensionEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionModuleIds",
|
||||
"columnName": "extensionModuleIds",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionFabIcon",
|
||||
"columnName": "extensionFabIcon",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "autoStartConfig",
|
||||
"columnName": "autoStartConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "forcedRunConfig",
|
||||
"columnName": "forcedRunConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "blackTechConfig",
|
||||
"columnName": "blackTechConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "disguiseConfig",
|
||||
"columnName": "disguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "browserDisguiseConfig",
|
||||
"columnName": "browserDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceDisguiseConfig",
|
||||
"columnName": "deviceDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationDialogConfig",
|
||||
"columnName": "activationDialogConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryId",
|
||||
"columnName": "categoryId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_web_apps_updatedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"updatedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_updatedAt` ON `${TABLE_NAME}` (`updatedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_categoryId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"categoryId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_categoryId` ON `${TABLE_NAME}` (`categoryId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_isActivated",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"isActivated"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_isActivated` ON `${TABLE_NAME}` (`isActivated`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_url` ON `${TABLE_NAME}` (`appType`, `url`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_iconPath_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"iconPath",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_iconPath_url` ON `${TABLE_NAME}` (`appType`, `iconPath`, `url`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_categories",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT NOT NULL, `color` TEXT NOT NULL, `sortOrder` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sortOrder",
|
||||
"columnName": "sortOrder",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "app_usage_stats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `launchCount` INTEGER NOT NULL, `totalUsageMs` INTEGER NOT NULL, `lastUsedAt` INTEGER NOT NULL, `lastSessionDurationMs` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "launchCount",
|
||||
"columnName": "launchCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "totalUsageMs",
|
||||
"columnName": "totalUsageMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUsedAt",
|
||||
"columnName": "lastUsedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSessionDurationMs",
|
||||
"columnName": "lastSessionDurationMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_usage_stats_appId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_app_usage_stats_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_lastUsedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"lastUsedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_lastUsedAt` ON `${TABLE_NAME}` (`lastUsedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_launchCount",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"launchCount"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_launchCount` ON `${TABLE_NAME}` (`launchCount`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_health_records",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `url` TEXT NOT NULL, `status` TEXT NOT NULL, `responseTimeMs` INTEGER NOT NULL, `httpStatusCode` INTEGER NOT NULL, `errorMessage` TEXT, `checkedAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "responseTimeMs",
|
||||
"columnName": "responseTimeMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpStatusCode",
|
||||
"columnName": "httpStatusCode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "errorMessage",
|
||||
"columnName": "errorMessage",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "checkedAt",
|
||||
"columnName": "checkedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_health_records_appId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_health_records_checkedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"checkedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_checkedAt` ON `${TABLE_NAME}` (`checkedAt`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '983cf11423592851e73dc30dd6bb36e3')"
|
||||
]
|
||||
}
|
||||
}
|
||||
578
app/schemas/com.webtoapp.data.database.AppDatabase/39.json
Normal file
578
app/schemas/com.webtoapp.data.database.AppDatabase/39.json
Normal file
@ -0,0 +1,578 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 39,
|
||||
"identityHash": "983cf11423592851e73dc30dd6bb36e3",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "web_apps",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `iconPath` TEXT, `packageName` TEXT, `appType` TEXT NOT NULL, `mediaConfig` TEXT, `galleryConfig` TEXT, `htmlConfig` TEXT, `wordpressConfig` TEXT, `nodejsConfig` TEXT, `phpAppConfig` TEXT, `pythonAppConfig` TEXT, `goAppConfig` TEXT, `multiWebConfig` TEXT, `activationEnabled` INTEGER NOT NULL, `activationCodeList` TEXT NOT NULL, `activationRequireEveryTime` INTEGER NOT NULL, `activationRemoteConfig` TEXT, `isActivated` INTEGER NOT NULL, `adsEnabled` INTEGER NOT NULL, `adConfig` TEXT, `announcementEnabled` INTEGER NOT NULL, `announcement` TEXT, `adBlockEnabled` INTEGER NOT NULL, `adBlockRules` TEXT NOT NULL, `adBlockSubscriptions` TEXT NOT NULL, `webViewConfig` TEXT NOT NULL, `splashEnabled` INTEGER NOT NULL, `splashConfig` TEXT, `bgmEnabled` INTEGER NOT NULL, `bgmConfig` TEXT, `apkExportConfig` TEXT, `themeType` TEXT NOT NULL, `translateEnabled` INTEGER NOT NULL, `translateConfig` TEXT, `extensionEnabled` INTEGER NOT NULL, `extensionModuleIds` TEXT NOT NULL, `extensionFabIcon` TEXT, `autoStartConfig` TEXT, `forcedRunConfig` TEXT, `blackTechConfig` TEXT, `disguiseConfig` TEXT, `browserDisguiseConfig` TEXT, `deviceDisguiseConfig` TEXT, `activationDialogConfig` TEXT, `categoryId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconPath",
|
||||
"columnName": "iconPath",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "packageName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "appType",
|
||||
"columnName": "appType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mediaConfig",
|
||||
"columnName": "mediaConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "galleryConfig",
|
||||
"columnName": "galleryConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "htmlConfig",
|
||||
"columnName": "htmlConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "wordpressConfig",
|
||||
"columnName": "wordpressConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "nodejsConfig",
|
||||
"columnName": "nodejsConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "phpAppConfig",
|
||||
"columnName": "phpAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pythonAppConfig",
|
||||
"columnName": "pythonAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "goAppConfig",
|
||||
"columnName": "goAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "multiWebConfig",
|
||||
"columnName": "multiWebConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationEnabled",
|
||||
"columnName": "activationEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationCodeList",
|
||||
"columnName": "activationCodeList",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRequireEveryTime",
|
||||
"columnName": "activationRequireEveryTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRemoteConfig",
|
||||
"columnName": "activationRemoteConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActivated",
|
||||
"columnName": "isActivated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adsEnabled",
|
||||
"columnName": "adsEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adConfig",
|
||||
"columnName": "adConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcementEnabled",
|
||||
"columnName": "announcementEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcement",
|
||||
"columnName": "announcement",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockEnabled",
|
||||
"columnName": "adBlockEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockRules",
|
||||
"columnName": "adBlockRules",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockSubscriptions",
|
||||
"columnName": "adBlockSubscriptions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "webViewConfig",
|
||||
"columnName": "webViewConfig",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashEnabled",
|
||||
"columnName": "splashEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashConfig",
|
||||
"columnName": "splashConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmEnabled",
|
||||
"columnName": "bgmEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmConfig",
|
||||
"columnName": "bgmConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "apkExportConfig",
|
||||
"columnName": "apkExportConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "themeType",
|
||||
"columnName": "themeType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateEnabled",
|
||||
"columnName": "translateEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateConfig",
|
||||
"columnName": "translateConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionEnabled",
|
||||
"columnName": "extensionEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionModuleIds",
|
||||
"columnName": "extensionModuleIds",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionFabIcon",
|
||||
"columnName": "extensionFabIcon",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "autoStartConfig",
|
||||
"columnName": "autoStartConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "forcedRunConfig",
|
||||
"columnName": "forcedRunConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "blackTechConfig",
|
||||
"columnName": "blackTechConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "disguiseConfig",
|
||||
"columnName": "disguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "browserDisguiseConfig",
|
||||
"columnName": "browserDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceDisguiseConfig",
|
||||
"columnName": "deviceDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationDialogConfig",
|
||||
"columnName": "activationDialogConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryId",
|
||||
"columnName": "categoryId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_web_apps_updatedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"updatedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_updatedAt` ON `${TABLE_NAME}` (`updatedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_categoryId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"categoryId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_categoryId` ON `${TABLE_NAME}` (`categoryId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_isActivated",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"isActivated"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_isActivated` ON `${TABLE_NAME}` (`isActivated`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_url` ON `${TABLE_NAME}` (`appType`, `url`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_iconPath_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"iconPath",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_iconPath_url` ON `${TABLE_NAME}` (`appType`, `iconPath`, `url`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_categories",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT NOT NULL, `color` TEXT NOT NULL, `sortOrder` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sortOrder",
|
||||
"columnName": "sortOrder",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "app_usage_stats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `launchCount` INTEGER NOT NULL, `totalUsageMs` INTEGER NOT NULL, `lastUsedAt` INTEGER NOT NULL, `lastSessionDurationMs` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "launchCount",
|
||||
"columnName": "launchCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "totalUsageMs",
|
||||
"columnName": "totalUsageMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUsedAt",
|
||||
"columnName": "lastUsedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSessionDurationMs",
|
||||
"columnName": "lastSessionDurationMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_usage_stats_appId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_app_usage_stats_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_lastUsedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"lastUsedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_lastUsedAt` ON `${TABLE_NAME}` (`lastUsedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_launchCount",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"launchCount"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_launchCount` ON `${TABLE_NAME}` (`launchCount`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_health_records",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `url` TEXT NOT NULL, `status` TEXT NOT NULL, `responseTimeMs` INTEGER NOT NULL, `httpStatusCode` INTEGER NOT NULL, `errorMessage` TEXT, `checkedAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "responseTimeMs",
|
||||
"columnName": "responseTimeMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpStatusCode",
|
||||
"columnName": "httpStatusCode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "errorMessage",
|
||||
"columnName": "errorMessage",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "checkedAt",
|
||||
"columnName": "checkedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_health_records_appId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_health_records_checkedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"checkedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_checkedAt` ON `${TABLE_NAME}` (`checkedAt`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '983cf11423592851e73dc30dd6bb36e3')"
|
||||
]
|
||||
}
|
||||
}
|
||||
578
app/schemas/com.webtoapp.data.database.AppDatabase/40.json
Normal file
578
app/schemas/com.webtoapp.data.database.AppDatabase/40.json
Normal file
@ -0,0 +1,578 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 40,
|
||||
"identityHash": "983cf11423592851e73dc30dd6bb36e3",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "web_apps",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `iconPath` TEXT, `packageName` TEXT, `appType` TEXT NOT NULL, `mediaConfig` TEXT, `galleryConfig` TEXT, `htmlConfig` TEXT, `wordpressConfig` TEXT, `nodejsConfig` TEXT, `phpAppConfig` TEXT, `pythonAppConfig` TEXT, `goAppConfig` TEXT, `multiWebConfig` TEXT, `activationEnabled` INTEGER NOT NULL, `activationCodeList` TEXT NOT NULL, `activationRequireEveryTime` INTEGER NOT NULL, `activationRemoteConfig` TEXT, `isActivated` INTEGER NOT NULL, `adsEnabled` INTEGER NOT NULL, `adConfig` TEXT, `announcementEnabled` INTEGER NOT NULL, `announcement` TEXT, `adBlockEnabled` INTEGER NOT NULL, `adBlockRules` TEXT NOT NULL, `adBlockSubscriptions` TEXT NOT NULL, `webViewConfig` TEXT NOT NULL, `splashEnabled` INTEGER NOT NULL, `splashConfig` TEXT, `bgmEnabled` INTEGER NOT NULL, `bgmConfig` TEXT, `apkExportConfig` TEXT, `themeType` TEXT NOT NULL, `translateEnabled` INTEGER NOT NULL, `translateConfig` TEXT, `extensionEnabled` INTEGER NOT NULL, `extensionModuleIds` TEXT NOT NULL, `extensionFabIcon` TEXT, `autoStartConfig` TEXT, `forcedRunConfig` TEXT, `blackTechConfig` TEXT, `disguiseConfig` TEXT, `browserDisguiseConfig` TEXT, `deviceDisguiseConfig` TEXT, `activationDialogConfig` TEXT, `categoryId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconPath",
|
||||
"columnName": "iconPath",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "packageName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "appType",
|
||||
"columnName": "appType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mediaConfig",
|
||||
"columnName": "mediaConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "galleryConfig",
|
||||
"columnName": "galleryConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "htmlConfig",
|
||||
"columnName": "htmlConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "wordpressConfig",
|
||||
"columnName": "wordpressConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "nodejsConfig",
|
||||
"columnName": "nodejsConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "phpAppConfig",
|
||||
"columnName": "phpAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pythonAppConfig",
|
||||
"columnName": "pythonAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "goAppConfig",
|
||||
"columnName": "goAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "multiWebConfig",
|
||||
"columnName": "multiWebConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationEnabled",
|
||||
"columnName": "activationEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationCodeList",
|
||||
"columnName": "activationCodeList",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRequireEveryTime",
|
||||
"columnName": "activationRequireEveryTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRemoteConfig",
|
||||
"columnName": "activationRemoteConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActivated",
|
||||
"columnName": "isActivated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adsEnabled",
|
||||
"columnName": "adsEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adConfig",
|
||||
"columnName": "adConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcementEnabled",
|
||||
"columnName": "announcementEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcement",
|
||||
"columnName": "announcement",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockEnabled",
|
||||
"columnName": "adBlockEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockRules",
|
||||
"columnName": "adBlockRules",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockSubscriptions",
|
||||
"columnName": "adBlockSubscriptions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "webViewConfig",
|
||||
"columnName": "webViewConfig",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashEnabled",
|
||||
"columnName": "splashEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashConfig",
|
||||
"columnName": "splashConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmEnabled",
|
||||
"columnName": "bgmEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmConfig",
|
||||
"columnName": "bgmConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "apkExportConfig",
|
||||
"columnName": "apkExportConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "themeType",
|
||||
"columnName": "themeType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateEnabled",
|
||||
"columnName": "translateEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateConfig",
|
||||
"columnName": "translateConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionEnabled",
|
||||
"columnName": "extensionEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionModuleIds",
|
||||
"columnName": "extensionModuleIds",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionFabIcon",
|
||||
"columnName": "extensionFabIcon",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "autoStartConfig",
|
||||
"columnName": "autoStartConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "forcedRunConfig",
|
||||
"columnName": "forcedRunConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "blackTechConfig",
|
||||
"columnName": "blackTechConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "disguiseConfig",
|
||||
"columnName": "disguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "browserDisguiseConfig",
|
||||
"columnName": "browserDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceDisguiseConfig",
|
||||
"columnName": "deviceDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationDialogConfig",
|
||||
"columnName": "activationDialogConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryId",
|
||||
"columnName": "categoryId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_web_apps_updatedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"updatedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_updatedAt` ON `${TABLE_NAME}` (`updatedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_categoryId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"categoryId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_categoryId` ON `${TABLE_NAME}` (`categoryId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_isActivated",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"isActivated"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_isActivated` ON `${TABLE_NAME}` (`isActivated`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_url` ON `${TABLE_NAME}` (`appType`, `url`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_iconPath_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"iconPath",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_iconPath_url` ON `${TABLE_NAME}` (`appType`, `iconPath`, `url`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_categories",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT NOT NULL, `color` TEXT NOT NULL, `sortOrder` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sortOrder",
|
||||
"columnName": "sortOrder",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "app_usage_stats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `launchCount` INTEGER NOT NULL, `totalUsageMs` INTEGER NOT NULL, `lastUsedAt` INTEGER NOT NULL, `lastSessionDurationMs` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "launchCount",
|
||||
"columnName": "launchCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "totalUsageMs",
|
||||
"columnName": "totalUsageMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUsedAt",
|
||||
"columnName": "lastUsedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSessionDurationMs",
|
||||
"columnName": "lastSessionDurationMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_usage_stats_appId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_app_usage_stats_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_lastUsedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"lastUsedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_lastUsedAt` ON `${TABLE_NAME}` (`lastUsedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_launchCount",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"launchCount"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_launchCount` ON `${TABLE_NAME}` (`launchCount`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_health_records",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `url` TEXT NOT NULL, `status` TEXT NOT NULL, `responseTimeMs` INTEGER NOT NULL, `httpStatusCode` INTEGER NOT NULL, `errorMessage` TEXT, `checkedAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "responseTimeMs",
|
||||
"columnName": "responseTimeMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpStatusCode",
|
||||
"columnName": "httpStatusCode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "errorMessage",
|
||||
"columnName": "errorMessage",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "checkedAt",
|
||||
"columnName": "checkedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_health_records_appId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_health_records_checkedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"checkedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_checkedAt` ON `${TABLE_NAME}` (`checkedAt`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '983cf11423592851e73dc30dd6bb36e3')"
|
||||
]
|
||||
}
|
||||
}
|
||||
573
app/schemas/com.webtoapp.data.database.AppDatabase/41.json
Normal file
573
app/schemas/com.webtoapp.data.database.AppDatabase/41.json
Normal file
@ -0,0 +1,573 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 41,
|
||||
"identityHash": "d5e920fa16e9de9b732ae1a50957f218",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "web_apps",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `iconPath` TEXT, `packageName` TEXT, `appType` TEXT NOT NULL, `mediaConfig` TEXT, `galleryConfig` TEXT, `htmlConfig` TEXT, `wordpressConfig` TEXT, `nodejsConfig` TEXT, `phpAppConfig` TEXT, `pythonAppConfig` TEXT, `goAppConfig` TEXT, `multiWebConfig` TEXT, `activationEnabled` INTEGER NOT NULL, `activationCodeList` TEXT NOT NULL, `activationRequireEveryTime` INTEGER NOT NULL, `activationRemoteConfig` TEXT, `isActivated` INTEGER NOT NULL, `adsEnabled` INTEGER NOT NULL, `adConfig` TEXT, `announcementEnabled` INTEGER NOT NULL, `announcement` TEXT, `adBlockEnabled` INTEGER NOT NULL, `adBlockRules` TEXT NOT NULL, `adBlockSubscriptions` TEXT NOT NULL, `webViewConfig` TEXT NOT NULL, `splashEnabled` INTEGER NOT NULL, `splashConfig` TEXT, `bgmEnabled` INTEGER NOT NULL, `bgmConfig` TEXT, `apkExportConfig` TEXT, `themeType` TEXT NOT NULL, `translateEnabled` INTEGER NOT NULL, `translateConfig` TEXT, `extensionEnabled` INTEGER NOT NULL, `extensionModuleIds` TEXT NOT NULL, `extensionFabIcon` TEXT, `autoStartConfig` TEXT, `forcedRunConfig` TEXT, `disguiseConfig` TEXT, `browserDisguiseConfig` TEXT, `deviceDisguiseConfig` TEXT, `activationDialogConfig` TEXT, `categoryId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconPath",
|
||||
"columnName": "iconPath",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "packageName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "appType",
|
||||
"columnName": "appType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mediaConfig",
|
||||
"columnName": "mediaConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "galleryConfig",
|
||||
"columnName": "galleryConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "htmlConfig",
|
||||
"columnName": "htmlConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "wordpressConfig",
|
||||
"columnName": "wordpressConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "nodejsConfig",
|
||||
"columnName": "nodejsConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "phpAppConfig",
|
||||
"columnName": "phpAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pythonAppConfig",
|
||||
"columnName": "pythonAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "goAppConfig",
|
||||
"columnName": "goAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "multiWebConfig",
|
||||
"columnName": "multiWebConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationEnabled",
|
||||
"columnName": "activationEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationCodeList",
|
||||
"columnName": "activationCodeList",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRequireEveryTime",
|
||||
"columnName": "activationRequireEveryTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRemoteConfig",
|
||||
"columnName": "activationRemoteConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActivated",
|
||||
"columnName": "isActivated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adsEnabled",
|
||||
"columnName": "adsEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adConfig",
|
||||
"columnName": "adConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcementEnabled",
|
||||
"columnName": "announcementEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcement",
|
||||
"columnName": "announcement",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockEnabled",
|
||||
"columnName": "adBlockEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockRules",
|
||||
"columnName": "adBlockRules",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockSubscriptions",
|
||||
"columnName": "adBlockSubscriptions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "webViewConfig",
|
||||
"columnName": "webViewConfig",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashEnabled",
|
||||
"columnName": "splashEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashConfig",
|
||||
"columnName": "splashConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmEnabled",
|
||||
"columnName": "bgmEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmConfig",
|
||||
"columnName": "bgmConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "apkExportConfig",
|
||||
"columnName": "apkExportConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "themeType",
|
||||
"columnName": "themeType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateEnabled",
|
||||
"columnName": "translateEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateConfig",
|
||||
"columnName": "translateConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionEnabled",
|
||||
"columnName": "extensionEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionModuleIds",
|
||||
"columnName": "extensionModuleIds",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionFabIcon",
|
||||
"columnName": "extensionFabIcon",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "autoStartConfig",
|
||||
"columnName": "autoStartConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "forcedRunConfig",
|
||||
"columnName": "forcedRunConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "disguiseConfig",
|
||||
"columnName": "disguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "browserDisguiseConfig",
|
||||
"columnName": "browserDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceDisguiseConfig",
|
||||
"columnName": "deviceDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationDialogConfig",
|
||||
"columnName": "activationDialogConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryId",
|
||||
"columnName": "categoryId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_web_apps_updatedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"updatedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_updatedAt` ON `${TABLE_NAME}` (`updatedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_categoryId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"categoryId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_categoryId` ON `${TABLE_NAME}` (`categoryId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_isActivated",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"isActivated"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_isActivated` ON `${TABLE_NAME}` (`isActivated`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_url` ON `${TABLE_NAME}` (`appType`, `url`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_iconPath_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"iconPath",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_iconPath_url` ON `${TABLE_NAME}` (`appType`, `iconPath`, `url`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_categories",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT NOT NULL, `color` TEXT NOT NULL, `sortOrder` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sortOrder",
|
||||
"columnName": "sortOrder",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "app_usage_stats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `launchCount` INTEGER NOT NULL, `totalUsageMs` INTEGER NOT NULL, `lastUsedAt` INTEGER NOT NULL, `lastSessionDurationMs` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "launchCount",
|
||||
"columnName": "launchCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "totalUsageMs",
|
||||
"columnName": "totalUsageMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUsedAt",
|
||||
"columnName": "lastUsedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSessionDurationMs",
|
||||
"columnName": "lastSessionDurationMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_usage_stats_appId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_app_usage_stats_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_lastUsedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"lastUsedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_lastUsedAt` ON `${TABLE_NAME}` (`lastUsedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_launchCount",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"launchCount"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_launchCount` ON `${TABLE_NAME}` (`launchCount`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_health_records",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `url` TEXT NOT NULL, `status` TEXT NOT NULL, `responseTimeMs` INTEGER NOT NULL, `httpStatusCode` INTEGER NOT NULL, `errorMessage` TEXT, `checkedAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "responseTimeMs",
|
||||
"columnName": "responseTimeMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpStatusCode",
|
||||
"columnName": "httpStatusCode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "errorMessage",
|
||||
"columnName": "errorMessage",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "checkedAt",
|
||||
"columnName": "checkedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_health_records_appId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_health_records_checkedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"checkedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_checkedAt` ON `${TABLE_NAME}` (`checkedAt`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd5e920fa16e9de9b732ae1a50957f218')"
|
||||
]
|
||||
}
|
||||
}
|
||||
568
app/schemas/com.webtoapp.data.database.AppDatabase/42.json
Normal file
568
app/schemas/com.webtoapp.data.database.AppDatabase/42.json
Normal file
@ -0,0 +1,568 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 42,
|
||||
"identityHash": "c7e4c9285c607540b9f15d79ad87c359",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "web_apps",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `iconPath` TEXT, `packageName` TEXT, `appType` TEXT NOT NULL, `mediaConfig` TEXT, `galleryConfig` TEXT, `htmlConfig` TEXT, `wordpressConfig` TEXT, `nodejsConfig` TEXT, `phpAppConfig` TEXT, `pythonAppConfig` TEXT, `goAppConfig` TEXT, `multiWebConfig` TEXT, `activationEnabled` INTEGER NOT NULL, `activationCodeList` TEXT NOT NULL, `activationRequireEveryTime` INTEGER NOT NULL, `activationRemoteConfig` TEXT, `isActivated` INTEGER NOT NULL, `adsEnabled` INTEGER NOT NULL, `adConfig` TEXT, `announcementEnabled` INTEGER NOT NULL, `announcement` TEXT, `adBlockEnabled` INTEGER NOT NULL, `adBlockRules` TEXT NOT NULL, `adBlockSubscriptions` TEXT NOT NULL, `webViewConfig` TEXT NOT NULL, `splashEnabled` INTEGER NOT NULL, `splashConfig` TEXT, `bgmEnabled` INTEGER NOT NULL, `bgmConfig` TEXT, `apkExportConfig` TEXT, `themeType` TEXT NOT NULL, `translateEnabled` INTEGER NOT NULL, `translateConfig` TEXT, `extensionEnabled` INTEGER NOT NULL, `extensionModuleIds` TEXT NOT NULL, `extensionFabIcon` TEXT, `autoStartConfig` TEXT, `disguiseConfig` TEXT, `browserDisguiseConfig` TEXT, `deviceDisguiseConfig` TEXT, `activationDialogConfig` TEXT, `categoryId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconPath",
|
||||
"columnName": "iconPath",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "packageName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "appType",
|
||||
"columnName": "appType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mediaConfig",
|
||||
"columnName": "mediaConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "galleryConfig",
|
||||
"columnName": "galleryConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "htmlConfig",
|
||||
"columnName": "htmlConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "wordpressConfig",
|
||||
"columnName": "wordpressConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "nodejsConfig",
|
||||
"columnName": "nodejsConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "phpAppConfig",
|
||||
"columnName": "phpAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pythonAppConfig",
|
||||
"columnName": "pythonAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "goAppConfig",
|
||||
"columnName": "goAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "multiWebConfig",
|
||||
"columnName": "multiWebConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationEnabled",
|
||||
"columnName": "activationEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationCodeList",
|
||||
"columnName": "activationCodeList",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRequireEveryTime",
|
||||
"columnName": "activationRequireEveryTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRemoteConfig",
|
||||
"columnName": "activationRemoteConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActivated",
|
||||
"columnName": "isActivated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adsEnabled",
|
||||
"columnName": "adsEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adConfig",
|
||||
"columnName": "adConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcementEnabled",
|
||||
"columnName": "announcementEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcement",
|
||||
"columnName": "announcement",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockEnabled",
|
||||
"columnName": "adBlockEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockRules",
|
||||
"columnName": "adBlockRules",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockSubscriptions",
|
||||
"columnName": "adBlockSubscriptions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "webViewConfig",
|
||||
"columnName": "webViewConfig",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashEnabled",
|
||||
"columnName": "splashEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashConfig",
|
||||
"columnName": "splashConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmEnabled",
|
||||
"columnName": "bgmEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmConfig",
|
||||
"columnName": "bgmConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "apkExportConfig",
|
||||
"columnName": "apkExportConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "themeType",
|
||||
"columnName": "themeType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateEnabled",
|
||||
"columnName": "translateEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateConfig",
|
||||
"columnName": "translateConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionEnabled",
|
||||
"columnName": "extensionEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionModuleIds",
|
||||
"columnName": "extensionModuleIds",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionFabIcon",
|
||||
"columnName": "extensionFabIcon",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "autoStartConfig",
|
||||
"columnName": "autoStartConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "disguiseConfig",
|
||||
"columnName": "disguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "browserDisguiseConfig",
|
||||
"columnName": "browserDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceDisguiseConfig",
|
||||
"columnName": "deviceDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationDialogConfig",
|
||||
"columnName": "activationDialogConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryId",
|
||||
"columnName": "categoryId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_web_apps_updatedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"updatedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_updatedAt` ON `${TABLE_NAME}` (`updatedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_categoryId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"categoryId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_categoryId` ON `${TABLE_NAME}` (`categoryId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_isActivated",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"isActivated"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_isActivated` ON `${TABLE_NAME}` (`isActivated`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_url` ON `${TABLE_NAME}` (`appType`, `url`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_iconPath_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"iconPath",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_iconPath_url` ON `${TABLE_NAME}` (`appType`, `iconPath`, `url`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_categories",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT NOT NULL, `color` TEXT NOT NULL, `sortOrder` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sortOrder",
|
||||
"columnName": "sortOrder",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "app_usage_stats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `launchCount` INTEGER NOT NULL, `totalUsageMs` INTEGER NOT NULL, `lastUsedAt` INTEGER NOT NULL, `lastSessionDurationMs` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "launchCount",
|
||||
"columnName": "launchCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "totalUsageMs",
|
||||
"columnName": "totalUsageMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUsedAt",
|
||||
"columnName": "lastUsedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSessionDurationMs",
|
||||
"columnName": "lastSessionDurationMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_usage_stats_appId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_app_usage_stats_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_lastUsedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"lastUsedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_lastUsedAt` ON `${TABLE_NAME}` (`lastUsedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_launchCount",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"launchCount"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_launchCount` ON `${TABLE_NAME}` (`launchCount`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_health_records",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `url` TEXT NOT NULL, `status` TEXT NOT NULL, `responseTimeMs` INTEGER NOT NULL, `httpStatusCode` INTEGER NOT NULL, `errorMessage` TEXT, `checkedAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "responseTimeMs",
|
||||
"columnName": "responseTimeMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpStatusCode",
|
||||
"columnName": "httpStatusCode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "errorMessage",
|
||||
"columnName": "errorMessage",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "checkedAt",
|
||||
"columnName": "checkedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_health_records_appId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_health_records_checkedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"checkedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_checkedAt` ON `${TABLE_NAME}` (`checkedAt`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c7e4c9285c607540b9f15d79ad87c359')"
|
||||
]
|
||||
}
|
||||
}
|
||||
563
app/schemas/com.webtoapp.data.database.AppDatabase/43.json
Normal file
563
app/schemas/com.webtoapp.data.database.AppDatabase/43.json
Normal file
@ -0,0 +1,563 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 43,
|
||||
"identityHash": "ea6b40911e9117204ec107ff693031d7",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "web_apps",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `iconPath` TEXT, `packageName` TEXT, `appType` TEXT NOT NULL, `mediaConfig` TEXT, `galleryConfig` TEXT, `htmlConfig` TEXT, `wordpressConfig` TEXT, `nodejsConfig` TEXT, `phpAppConfig` TEXT, `pythonAppConfig` TEXT, `goAppConfig` TEXT, `multiWebConfig` TEXT, `activationEnabled` INTEGER NOT NULL, `activationCodeList` TEXT NOT NULL, `activationRequireEveryTime` INTEGER NOT NULL, `activationRemoteConfig` TEXT, `isActivated` INTEGER NOT NULL, `adsEnabled` INTEGER NOT NULL, `adConfig` TEXT, `announcementEnabled` INTEGER NOT NULL, `announcement` TEXT, `adBlockEnabled` INTEGER NOT NULL, `adBlockRules` TEXT NOT NULL, `adBlockSubscriptions` TEXT NOT NULL, `webViewConfig` TEXT NOT NULL, `splashEnabled` INTEGER NOT NULL, `splashConfig` TEXT, `bgmEnabled` INTEGER NOT NULL, `bgmConfig` TEXT, `apkExportConfig` TEXT, `themeType` TEXT NOT NULL, `translateEnabled` INTEGER NOT NULL, `translateConfig` TEXT, `extensionEnabled` INTEGER NOT NULL, `extensionModuleIds` TEXT NOT NULL, `extensionFabIcon` TEXT, `autoStartConfig` TEXT, `browserDisguiseConfig` TEXT, `deviceDisguiseConfig` TEXT, `activationDialogConfig` TEXT, `categoryId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconPath",
|
||||
"columnName": "iconPath",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "packageName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "appType",
|
||||
"columnName": "appType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mediaConfig",
|
||||
"columnName": "mediaConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "galleryConfig",
|
||||
"columnName": "galleryConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "htmlConfig",
|
||||
"columnName": "htmlConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "wordpressConfig",
|
||||
"columnName": "wordpressConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "nodejsConfig",
|
||||
"columnName": "nodejsConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "phpAppConfig",
|
||||
"columnName": "phpAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pythonAppConfig",
|
||||
"columnName": "pythonAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "goAppConfig",
|
||||
"columnName": "goAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "multiWebConfig",
|
||||
"columnName": "multiWebConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationEnabled",
|
||||
"columnName": "activationEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationCodeList",
|
||||
"columnName": "activationCodeList",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRequireEveryTime",
|
||||
"columnName": "activationRequireEveryTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRemoteConfig",
|
||||
"columnName": "activationRemoteConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActivated",
|
||||
"columnName": "isActivated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adsEnabled",
|
||||
"columnName": "adsEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adConfig",
|
||||
"columnName": "adConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcementEnabled",
|
||||
"columnName": "announcementEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcement",
|
||||
"columnName": "announcement",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockEnabled",
|
||||
"columnName": "adBlockEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockRules",
|
||||
"columnName": "adBlockRules",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockSubscriptions",
|
||||
"columnName": "adBlockSubscriptions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "webViewConfig",
|
||||
"columnName": "webViewConfig",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashEnabled",
|
||||
"columnName": "splashEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashConfig",
|
||||
"columnName": "splashConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmEnabled",
|
||||
"columnName": "bgmEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmConfig",
|
||||
"columnName": "bgmConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "apkExportConfig",
|
||||
"columnName": "apkExportConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "themeType",
|
||||
"columnName": "themeType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateEnabled",
|
||||
"columnName": "translateEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateConfig",
|
||||
"columnName": "translateConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionEnabled",
|
||||
"columnName": "extensionEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionModuleIds",
|
||||
"columnName": "extensionModuleIds",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionFabIcon",
|
||||
"columnName": "extensionFabIcon",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "autoStartConfig",
|
||||
"columnName": "autoStartConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "browserDisguiseConfig",
|
||||
"columnName": "browserDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceDisguiseConfig",
|
||||
"columnName": "deviceDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationDialogConfig",
|
||||
"columnName": "activationDialogConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryId",
|
||||
"columnName": "categoryId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_web_apps_updatedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"updatedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_updatedAt` ON `${TABLE_NAME}` (`updatedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_categoryId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"categoryId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_categoryId` ON `${TABLE_NAME}` (`categoryId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_isActivated",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"isActivated"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_isActivated` ON `${TABLE_NAME}` (`isActivated`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_url` ON `${TABLE_NAME}` (`appType`, `url`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_iconPath_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"iconPath",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_iconPath_url` ON `${TABLE_NAME}` (`appType`, `iconPath`, `url`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_categories",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT NOT NULL, `color` TEXT NOT NULL, `sortOrder` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sortOrder",
|
||||
"columnName": "sortOrder",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "app_usage_stats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `launchCount` INTEGER NOT NULL, `totalUsageMs` INTEGER NOT NULL, `lastUsedAt` INTEGER NOT NULL, `lastSessionDurationMs` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "launchCount",
|
||||
"columnName": "launchCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "totalUsageMs",
|
||||
"columnName": "totalUsageMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUsedAt",
|
||||
"columnName": "lastUsedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSessionDurationMs",
|
||||
"columnName": "lastSessionDurationMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_usage_stats_appId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_app_usage_stats_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_lastUsedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"lastUsedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_lastUsedAt` ON `${TABLE_NAME}` (`lastUsedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_launchCount",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"launchCount"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_launchCount` ON `${TABLE_NAME}` (`launchCount`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_health_records",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `url` TEXT NOT NULL, `status` TEXT NOT NULL, `responseTimeMs` INTEGER NOT NULL, `httpStatusCode` INTEGER NOT NULL, `errorMessage` TEXT, `checkedAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "responseTimeMs",
|
||||
"columnName": "responseTimeMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpStatusCode",
|
||||
"columnName": "httpStatusCode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "errorMessage",
|
||||
"columnName": "errorMessage",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "checkedAt",
|
||||
"columnName": "checkedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_health_records_appId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_health_records_checkedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"checkedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_checkedAt` ON `${TABLE_NAME}` (`checkedAt`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ea6b40911e9117204ec107ff693031d7')"
|
||||
]
|
||||
}
|
||||
}
|
||||
568
app/schemas/com.webtoapp.data.database.AppDatabase/44.json
Normal file
568
app/schemas/com.webtoapp.data.database.AppDatabase/44.json
Normal file
@ -0,0 +1,568 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 44,
|
||||
"identityHash": "0a034258a95fdd6ab5f7d94147e9de43",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "web_apps",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `iconPath` TEXT, `packageName` TEXT, `appType` TEXT NOT NULL, `mediaConfig` TEXT, `galleryConfig` TEXT, `htmlConfig` TEXT, `wordpressConfig` TEXT, `nodejsConfig` TEXT, `phpAppConfig` TEXT, `pythonAppConfig` TEXT, `goAppConfig` TEXT, `multiWebConfig` TEXT, `activationEnabled` INTEGER NOT NULL, `activationCodeList` TEXT NOT NULL, `activationRequireEveryTime` INTEGER NOT NULL, `activationRemoteConfig` TEXT, `isActivated` INTEGER NOT NULL, `appLockConfig` TEXT, `adsEnabled` INTEGER NOT NULL, `adConfig` TEXT, `announcementEnabled` INTEGER NOT NULL, `announcement` TEXT, `adBlockEnabled` INTEGER NOT NULL, `adBlockRules` TEXT NOT NULL, `adBlockSubscriptions` TEXT NOT NULL, `webViewConfig` TEXT NOT NULL, `splashEnabled` INTEGER NOT NULL, `splashConfig` TEXT, `bgmEnabled` INTEGER NOT NULL, `bgmConfig` TEXT, `apkExportConfig` TEXT, `themeType` TEXT NOT NULL, `translateEnabled` INTEGER NOT NULL, `translateConfig` TEXT, `extensionEnabled` INTEGER NOT NULL, `extensionModuleIds` TEXT NOT NULL, `extensionFabIcon` TEXT, `autoStartConfig` TEXT, `browserDisguiseConfig` TEXT, `deviceDisguiseConfig` TEXT, `activationDialogConfig` TEXT, `categoryId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconPath",
|
||||
"columnName": "iconPath",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "packageName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "appType",
|
||||
"columnName": "appType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mediaConfig",
|
||||
"columnName": "mediaConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "galleryConfig",
|
||||
"columnName": "galleryConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "htmlConfig",
|
||||
"columnName": "htmlConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "wordpressConfig",
|
||||
"columnName": "wordpressConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "nodejsConfig",
|
||||
"columnName": "nodejsConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "phpAppConfig",
|
||||
"columnName": "phpAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pythonAppConfig",
|
||||
"columnName": "pythonAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "goAppConfig",
|
||||
"columnName": "goAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "multiWebConfig",
|
||||
"columnName": "multiWebConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationEnabled",
|
||||
"columnName": "activationEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationCodeList",
|
||||
"columnName": "activationCodeList",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRequireEveryTime",
|
||||
"columnName": "activationRequireEveryTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRemoteConfig",
|
||||
"columnName": "activationRemoteConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActivated",
|
||||
"columnName": "isActivated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appLockConfig",
|
||||
"columnName": "appLockConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "adsEnabled",
|
||||
"columnName": "adsEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adConfig",
|
||||
"columnName": "adConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcementEnabled",
|
||||
"columnName": "announcementEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcement",
|
||||
"columnName": "announcement",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockEnabled",
|
||||
"columnName": "adBlockEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockRules",
|
||||
"columnName": "adBlockRules",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockSubscriptions",
|
||||
"columnName": "adBlockSubscriptions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "webViewConfig",
|
||||
"columnName": "webViewConfig",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashEnabled",
|
||||
"columnName": "splashEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashConfig",
|
||||
"columnName": "splashConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmEnabled",
|
||||
"columnName": "bgmEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmConfig",
|
||||
"columnName": "bgmConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "apkExportConfig",
|
||||
"columnName": "apkExportConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "themeType",
|
||||
"columnName": "themeType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateEnabled",
|
||||
"columnName": "translateEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateConfig",
|
||||
"columnName": "translateConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionEnabled",
|
||||
"columnName": "extensionEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionModuleIds",
|
||||
"columnName": "extensionModuleIds",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionFabIcon",
|
||||
"columnName": "extensionFabIcon",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "autoStartConfig",
|
||||
"columnName": "autoStartConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "browserDisguiseConfig",
|
||||
"columnName": "browserDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceDisguiseConfig",
|
||||
"columnName": "deviceDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationDialogConfig",
|
||||
"columnName": "activationDialogConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryId",
|
||||
"columnName": "categoryId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_web_apps_updatedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"updatedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_updatedAt` ON `${TABLE_NAME}` (`updatedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_categoryId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"categoryId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_categoryId` ON `${TABLE_NAME}` (`categoryId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_isActivated",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"isActivated"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_isActivated` ON `${TABLE_NAME}` (`isActivated`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_url` ON `${TABLE_NAME}` (`appType`, `url`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_iconPath_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"iconPath",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_iconPath_url` ON `${TABLE_NAME}` (`appType`, `iconPath`, `url`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_categories",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT NOT NULL, `color` TEXT NOT NULL, `sortOrder` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sortOrder",
|
||||
"columnName": "sortOrder",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "app_usage_stats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `launchCount` INTEGER NOT NULL, `totalUsageMs` INTEGER NOT NULL, `lastUsedAt` INTEGER NOT NULL, `lastSessionDurationMs` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "launchCount",
|
||||
"columnName": "launchCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "totalUsageMs",
|
||||
"columnName": "totalUsageMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUsedAt",
|
||||
"columnName": "lastUsedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSessionDurationMs",
|
||||
"columnName": "lastSessionDurationMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_usage_stats_appId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_app_usage_stats_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_lastUsedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"lastUsedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_lastUsedAt` ON `${TABLE_NAME}` (`lastUsedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_launchCount",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"launchCount"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_launchCount` ON `${TABLE_NAME}` (`launchCount`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_health_records",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `url` TEXT NOT NULL, `status` TEXT NOT NULL, `responseTimeMs` INTEGER NOT NULL, `httpStatusCode` INTEGER NOT NULL, `errorMessage` TEXT, `checkedAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "responseTimeMs",
|
||||
"columnName": "responseTimeMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpStatusCode",
|
||||
"columnName": "httpStatusCode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "errorMessage",
|
||||
"columnName": "errorMessage",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "checkedAt",
|
||||
"columnName": "checkedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_health_records_appId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_health_records_checkedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"checkedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_checkedAt` ON `${TABLE_NAME}` (`checkedAt`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0a034258a95fdd6ab5f7d94147e9de43')"
|
||||
]
|
||||
}
|
||||
}
|
||||
563
app/schemas/com.webtoapp.data.database.AppDatabase/45.json
Normal file
563
app/schemas/com.webtoapp.data.database.AppDatabase/45.json
Normal file
@ -0,0 +1,563 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 45,
|
||||
"identityHash": "ea6b40911e9117204ec107ff693031d7",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "web_apps",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `iconPath` TEXT, `packageName` TEXT, `appType` TEXT NOT NULL, `mediaConfig` TEXT, `galleryConfig` TEXT, `htmlConfig` TEXT, `wordpressConfig` TEXT, `nodejsConfig` TEXT, `phpAppConfig` TEXT, `pythonAppConfig` TEXT, `goAppConfig` TEXT, `multiWebConfig` TEXT, `activationEnabled` INTEGER NOT NULL, `activationCodeList` TEXT NOT NULL, `activationRequireEveryTime` INTEGER NOT NULL, `activationRemoteConfig` TEXT, `isActivated` INTEGER NOT NULL, `adsEnabled` INTEGER NOT NULL, `adConfig` TEXT, `announcementEnabled` INTEGER NOT NULL, `announcement` TEXT, `adBlockEnabled` INTEGER NOT NULL, `adBlockRules` TEXT NOT NULL, `adBlockSubscriptions` TEXT NOT NULL, `webViewConfig` TEXT NOT NULL, `splashEnabled` INTEGER NOT NULL, `splashConfig` TEXT, `bgmEnabled` INTEGER NOT NULL, `bgmConfig` TEXT, `apkExportConfig` TEXT, `themeType` TEXT NOT NULL, `translateEnabled` INTEGER NOT NULL, `translateConfig` TEXT, `extensionEnabled` INTEGER NOT NULL, `extensionModuleIds` TEXT NOT NULL, `extensionFabIcon` TEXT, `autoStartConfig` TEXT, `browserDisguiseConfig` TEXT, `deviceDisguiseConfig` TEXT, `activationDialogConfig` TEXT, `categoryId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconPath",
|
||||
"columnName": "iconPath",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "packageName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "appType",
|
||||
"columnName": "appType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mediaConfig",
|
||||
"columnName": "mediaConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "galleryConfig",
|
||||
"columnName": "galleryConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "htmlConfig",
|
||||
"columnName": "htmlConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "wordpressConfig",
|
||||
"columnName": "wordpressConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "nodejsConfig",
|
||||
"columnName": "nodejsConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "phpAppConfig",
|
||||
"columnName": "phpAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pythonAppConfig",
|
||||
"columnName": "pythonAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "goAppConfig",
|
||||
"columnName": "goAppConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "multiWebConfig",
|
||||
"columnName": "multiWebConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationEnabled",
|
||||
"columnName": "activationEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationCodeList",
|
||||
"columnName": "activationCodeList",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRequireEveryTime",
|
||||
"columnName": "activationRequireEveryTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationRemoteConfig",
|
||||
"columnName": "activationRemoteConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActivated",
|
||||
"columnName": "isActivated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adsEnabled",
|
||||
"columnName": "adsEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adConfig",
|
||||
"columnName": "adConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcementEnabled",
|
||||
"columnName": "announcementEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "announcement",
|
||||
"columnName": "announcement",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockEnabled",
|
||||
"columnName": "adBlockEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockRules",
|
||||
"columnName": "adBlockRules",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "adBlockSubscriptions",
|
||||
"columnName": "adBlockSubscriptions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "webViewConfig",
|
||||
"columnName": "webViewConfig",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashEnabled",
|
||||
"columnName": "splashEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "splashConfig",
|
||||
"columnName": "splashConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmEnabled",
|
||||
"columnName": "bgmEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bgmConfig",
|
||||
"columnName": "bgmConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "apkExportConfig",
|
||||
"columnName": "apkExportConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "themeType",
|
||||
"columnName": "themeType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateEnabled",
|
||||
"columnName": "translateEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translateConfig",
|
||||
"columnName": "translateConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionEnabled",
|
||||
"columnName": "extensionEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionModuleIds",
|
||||
"columnName": "extensionModuleIds",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extensionFabIcon",
|
||||
"columnName": "extensionFabIcon",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "autoStartConfig",
|
||||
"columnName": "autoStartConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "browserDisguiseConfig",
|
||||
"columnName": "browserDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceDisguiseConfig",
|
||||
"columnName": "deviceDisguiseConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationDialogConfig",
|
||||
"columnName": "activationDialogConfig",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryId",
|
||||
"columnName": "categoryId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_web_apps_updatedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"updatedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_updatedAt` ON `${TABLE_NAME}` (`updatedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_categoryId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"categoryId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_categoryId` ON `${TABLE_NAME}` (`categoryId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_isActivated",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"isActivated"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_isActivated` ON `${TABLE_NAME}` (`isActivated`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_url` ON `${TABLE_NAME}` (`appType`, `url`)"
|
||||
},
|
||||
{
|
||||
"name": "index_web_apps_appType_iconPath_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appType",
|
||||
"iconPath",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_web_apps_appType_iconPath_url` ON `${TABLE_NAME}` (`appType`, `iconPath`, `url`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_categories",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT NOT NULL, `color` TEXT NOT NULL, `sortOrder` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sortOrder",
|
||||
"columnName": "sortOrder",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "app_usage_stats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `launchCount` INTEGER NOT NULL, `totalUsageMs` INTEGER NOT NULL, `lastUsedAt` INTEGER NOT NULL, `lastSessionDurationMs` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "launchCount",
|
||||
"columnName": "launchCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "totalUsageMs",
|
||||
"columnName": "totalUsageMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUsedAt",
|
||||
"columnName": "lastUsedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSessionDurationMs",
|
||||
"columnName": "lastSessionDurationMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_usage_stats_appId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_app_usage_stats_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_lastUsedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"lastUsedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_lastUsedAt` ON `${TABLE_NAME}` (`lastUsedAt`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_usage_stats_launchCount",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"launchCount"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_usage_stats_launchCount` ON `${TABLE_NAME}` (`launchCount`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "app_health_records",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `appId` INTEGER NOT NULL, `url` TEXT NOT NULL, `status` TEXT NOT NULL, `responseTimeMs` INTEGER NOT NULL, `httpStatusCode` INTEGER NOT NULL, `errorMessage` TEXT, `checkedAt` INTEGER NOT NULL, FOREIGN KEY(`appId`) REFERENCES `web_apps`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "appId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "responseTimeMs",
|
||||
"columnName": "responseTimeMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpStatusCode",
|
||||
"columnName": "httpStatusCode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "errorMessage",
|
||||
"columnName": "errorMessage",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "checkedAt",
|
||||
"columnName": "checkedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_health_records_appId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"appId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_appId` ON `${TABLE_NAME}` (`appId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_health_records_checkedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"checkedAt"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_health_records_checkedAt` ON `${TABLE_NAME}` (`checkedAt`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "web_apps",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"appId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ea6b40911e9117204ec107ff693031d7')"
|
||||
]
|
||||
}
|
||||
}
|
||||
17
app/src/debug/res/xml/network_security_config.xml
Normal file
17
app/src/debug/res/xml/network_security_config.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">localhost</domain>
|
||||
<domain includeSubdomains="false">127.0.0.1</domain>
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
522
app/src/main/AndroidManifest.xml
Normal file
522
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,522 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!--
|
||||
==========================================================================
|
||||
权限声明策略(2026-05 修订)
|
||||
==========================================================================
|
||||
本 manifest 只声明「宿主 app 本体」运行需要的权限和组件。
|
||||
|
||||
设计原则:
|
||||
1. 用户打包的子 APK 如需 SMS / 通话 / 通讯录 / 日历 / 定位 / 传感器 /
|
||||
NFC / USB / 后台服务 等高危能力,由 ApkBuilder 在打包时按用户勾选
|
||||
动态注入到**子包**的 manifest,不预声明在宿主 manifest 里。
|
||||
2. 仅供子 APK 使用的运行时组件(如后台运行服务 / Node.js 子进程前台
|
||||
服务等)只在 shell 模板 manifest 中声明,不混入宿主 manifest。
|
||||
宿主自身不会实例化这些组件,预声明只会让国产安全引擎做"组件指纹
|
||||
聚合"时把宿主误判为驻留型可疑应用。
|
||||
|
||||
若有新的宿主功能真的需要某个权限/组件,请:
|
||||
1. 在本文件添加并注释"宿主使用:xxx 功能"
|
||||
2. 在代码里能被 grep 到对应 API 调用
|
||||
禁止再把"给子 APK 准备"的权限/组件写进本文件。
|
||||
==========================================================================
|
||||
-->
|
||||
|
||||
<!-- ====== 网络(宿主必需:加载网页、下载运行时、推送请求)====== -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<!-- 宿主使用:预览/分析局域网地址。Android 16 opt-in、17 强制(部分 ROM 不分
|
||||
targetSdk 直接强制);缺失时 LAN 请求超时或报 ERR_LOCAL_NETWORK_PERMISSION_MISSING。
|
||||
旧系统会忽略未知权限,无副作用。 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
|
||||
|
||||
<!-- ====== 存储 / 媒体(宿主必需:选择图标、导入 HTML、素材库)====== -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
||||
|
||||
<!-- ====== APK 安装 / 桌面快捷方式(宿主必需:打包完成后一键安装子 APK)====== -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||
|
||||
<!-- ====== 通知(宿主必需:打包任务前台通知)====== -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- ====== 相机(宿主使用:扫码导入网址)====== -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<!-- ====== 音频(宿主使用:录音、媒体回放音量调整)====== -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<!-- ====== 定位(宿主使用:预览需要地理位置的网站,与生成的 APK 保持一致)====== -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<!-- ====== WiFi(宿主使用:网络状态查询、WiFi 扫描;写权限留给子 APK 注入)====== -->
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
|
||||
<!-- ====== 蓝牙(宿主使用:基本扫描/连接)====== -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"
|
||||
android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
|
||||
android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<!-- ====== 生物识别(宿主使用:应用锁、隐私保护)====== -->
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
|
||||
<!-- ====== 悬浮窗(宿主使用:FloatingWindowService 悬浮小窗预览模式)====== -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<!-- ====== 电池 / 勿扰(宿主使用:打包任务防杀后台、勿扰模式)====== -->
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
||||
|
||||
<!-- ====== 前台服务(宿主必需:打包任务 + 悬浮窗预览 + WebView 媒体播放)====== -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
|
||||
<!-- ====== 系统运行(宿主必需:打包 / 启动等长任务保持唤醒)====== -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!-- ====== 自启动(宿主使用:BootReceiver 开机拉起用户指定的子应用、定时启动)====== -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
||||
|
||||
<!-- ====== uses-feature(标注设备能力,全部 required=false 不限制安装)====== -->
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.flash" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.microphone" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.wifi" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.screen.portrait" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.screen.landscape" android:required="false" />
|
||||
|
||||
|
||||
<queries>
|
||||
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="tel" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="mailto" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="sms" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="smsto" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="mms" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="mmsto" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="geo" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="market" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="magnet" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="thunder" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="ed2k" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="*" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
|
||||
<package android:name="idm.internet.download.manager" />
|
||||
<package android:name="idm.internet.download.manager.plus" />
|
||||
<package android:name="idm.internet.download.manager.adm.lite" />
|
||||
<package android:name="com.dv.adm" />
|
||||
<package android:name="com.dv.adm.pay" />
|
||||
<package android:name="com.mxtech.videoplayer.ad" />
|
||||
<package android:name="com.mxtech.videoplayer.pro" />
|
||||
<package android:name="org.videolan.vlc" />
|
||||
<package android:name="com.kmplayer" />
|
||||
<package android:name="com.xunlei.downloadprovider" />
|
||||
|
||||
<intent>
|
||||
<action android:name="android.media.action.IMAGE_CAPTURE" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.media.action.VIDEO_CAPTURE" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="br.com.drukstech.codeapp.action.PORT_QUERY" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="br.com.drukstech.codeapp.action.PORT_RELEASE" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<!--
|
||||
==========================================================================
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
==========================================================================
|
||||
Android 13+ 的「可预测返回手势」(predictive back) 要求 app 显式声明这个
|
||||
flag,否则 OEM(华为 / 小米 / OPPO / 三星等)的侧边返回手势 / 三角形
|
||||
返回键会**绕过**老的 OnBackPressedDispatcher,默认行为是 finish() 当前
|
||||
Activity,于是用户在 webapp 里滑边返回会**直接退到桌面**而不是回退到
|
||||
上一个网页。
|
||||
|
||||
打开这个 flag 后,AndroidX activity 1.8+ 的 OnBackPressedDispatcher 会
|
||||
自动桥接到系统的 OnBackInvokedDispatcher——所有现有的
|
||||
OnBackPressedCallback / BackHandler 不需要改动,就能在 Android 13+
|
||||
收到手势/三角键的返回事件,按已实现的 WebView.goBack() 逻辑做网页历史
|
||||
回退。
|
||||
|
||||
Android 12 及以下忽略此 flag,行为与原本完全一致。
|
||||
==========================================================================
|
||||
-->
|
||||
<application
|
||||
android:name=".DruksCodeApplication"
|
||||
android:allowBackup="false"
|
||||
android:largeHeap="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.DruksCode"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:pageSizeCompat="enabled"
|
||||
tools:targetApi="31">
|
||||
|
||||
|
||||
<meta-data
|
||||
android:name="br.com.drukstech.codeapp.DKC_APP_MARKER"
|
||||
android:value="true" />
|
||||
|
||||
|
||||
<property
|
||||
android:name="android.app.property.PAGE_SIZE_COMPATIBILITY"
|
||||
android:value="16" />
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/Theme.DruksCode">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- OAuth callback -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="br.com.drukstech.codeapp"
|
||||
android:host="oauth2callback" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Browser intent: handle http/https URLs so Android offers DruksCode as a browser -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Share intent: receive text/URLs from other apps via share sheet -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".ui.webview.WebViewActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize|uiMode"
|
||||
android:exported="false"
|
||||
android:hardwareAccelerated="true"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/Theme.DruksCode.Shell" />
|
||||
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".ui.shell.ShellActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize|uiMode"
|
||||
android:exported="false"
|
||||
android:hardwareAccelerated="true"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/Theme.DruksCode.Shell" />
|
||||
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".ui.media.MediaAppActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize"
|
||||
android:exported="false"
|
||||
android:hardwareAccelerated="true"
|
||||
android:theme="@style/Theme.DruksCode.Fullscreen" />
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".ui.gallery.GalleryPlayerActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize"
|
||||
android:exported="false"
|
||||
android:hardwareAccelerated="true"
|
||||
android:theme="@style/Theme.DruksCode.Fullscreen" />
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".ui.codepreview.HtmlPreviewActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:exported="false"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/Theme.DruksCode" />
|
||||
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".ui.splash.SplashLauncherActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:noHistory="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/Theme.DruksCode.Transparent" />
|
||||
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
|
||||
<service
|
||||
android:name=".core.agent.runtime.AgentService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<!--
|
||||
WebMediaPlaybackService - WebView 内媒体播放的后台保活前台服务。
|
||||
音频本身仍在 WebView 内播放;该服务提供 mediaPlayback 前台服务 + 唤醒锁 +
|
||||
媒体通知生命周期(播放中启动 / 暂停分离 / 停止移除)。
|
||||
-->
|
||||
<service
|
||||
android:name=".core.webview.WebMediaPlaybackService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:stopWithTask="false" />
|
||||
|
||||
<!--
|
||||
NodeService - Node.js 嵌入式运行时的独立子进程容器。
|
||||
|
||||
为什么单独开一个 OS 进程:
|
||||
libnode.so 一旦 dlopen + V8 init 就**永远 reset 不了**。如果跑在主进程:
|
||||
1) 用户 server.js 抛 SyntaxError / 调 process.exit 会让宿主进程死
|
||||
2) 切换项目 / 上次崩了想重试都没法做,必须用户从最近任务列表把
|
||||
App 划掉再重开——非常糟糕的用户体验
|
||||
把它关到 :nodejs 子进程后,V8 死活都关在子进程里,主进程稳如磐石。
|
||||
要重启 V8 时主进程让子进程自杀,Android Framework 自动重建——零感知。
|
||||
|
||||
android:process=":nodejs" → 同 APK 内的独立 OS 进程(前导冒号表示
|
||||
private process,跟 webtoapp 共享 UID
|
||||
但有独立的 zygote-fork 出来的进程)
|
||||
isolatedProcess=false → 我们要访问 filesDir / nativeLibraryDir,
|
||||
isolatedProcess 模式下连这些都拿不到
|
||||
stopWithTask=false → 用户从最近任务划掉 App 时仍然让子进程
|
||||
一起死(true 是默认值,写出来是为了
|
||||
清楚——避免误以为子进程会比主进程
|
||||
活更久)
|
||||
-->
|
||||
<service
|
||||
android:name=".core.nodejs.NodeService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":nodejs"
|
||||
android:stopWithTask="true" />
|
||||
|
||||
<!--
|
||||
BootReceiver / ScheduledStartReceiver
|
||||
宿主使用:用户在 host 内可配置"开机自启某个 webapp"和"定时启动";
|
||||
BootReceiver 同时也兼容 shell 子 APK(行为等价的 java-override,
|
||||
含开机/更新后的通知服务恢复——改动需两侧同步)。
|
||||
-->
|
||||
<receiver
|
||||
android:name=".core.autostart.BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".core.autostart.ScheduledStartReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="br.com.drukstech.codeapp.ACTION_SCHEDULED_START" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!--
|
||||
Port query / release receivers
|
||||
宿主使用:跨 webapp 端口协调(Node/PHP/Python/Go 多运行时共存时)。
|
||||
exported=true 是因为多个由 DruksCode 打包出的 APK 之间需要互相广播查询端口。
|
||||
-->
|
||||
<receiver
|
||||
android:name=".core.port.PortQueryReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="br.com.drukstech.codeapp.action.PORT_QUERY" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".core.port.PortReleaseReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="br.com.drukstech.codeapp.action.PORT_RELEASE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!--
|
||||
BridgeAlarmReceiver - 通知调度(页内 Web Notification.scheduleNotification)。
|
||||
宿主与生成的 APK 均使用(NativeBridge 为 shell 同步代码),shell 模板清单
|
||||
必须保留同名声明,两边需同步修改。
|
||||
-->
|
||||
<receiver
|
||||
android:name=".core.notification.BridgeAlarmReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="br.com.drukstech.codeapp.bridge.SCHEDULED_NOTIFICATION" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".core.background.BackgroundRunService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="background_run" />
|
||||
</service>
|
||||
|
||||
|
||||
<service
|
||||
android:name=".core.notification.NotificationPollingService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="notification_polling" />
|
||||
</service>
|
||||
|
||||
|
||||
<service
|
||||
android:name=".core.notification.NotificationWebSocketService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="notification_websocket" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".core.notification.NotificationFcmService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
|
||||
|
||||
<service
|
||||
android:name=".core.floatingwindow.FloatingWindowService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="floating_window" />
|
||||
</service>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
16
app/src/main/assets/bgm/README.txt
Normal file
16
app/src/main/assets/bgm/README.txt
Normal file
@ -0,0 +1,16 @@
|
||||
背景音乐预置目录
|
||||
================
|
||||
|
||||
将 MP3 音乐文件放入此目录即可作为预置背景音乐使用。
|
||||
|
||||
文件命名规则:
|
||||
- 音乐文件:小乔.mp3
|
||||
- 封面图片:小乔.png(或 .jpg/.jpeg)
|
||||
|
||||
同名的音乐和图片会自动配对,图片将作为音乐的封面图标显示。
|
||||
封面图片是可选的,仅用于在选择界面方便识别。
|
||||
|
||||
示例:
|
||||
- 小乔.mp3 + 小乔.png
|
||||
- 背景音乐1.mp3 + 背景音乐1.jpg
|
||||
- bgm_01.mp3(无封面)
|
||||
698
app/src/main/assets/php_router_server.php
Normal file
698
app/src/main/assets/php_router_server.php
Normal file
@ -0,0 +1,698 @@
|
||||
<?php
|
||||
|
||||
set_time_limit(0);
|
||||
|
||||
$port = (int)($argv[1] ?? 8080);
|
||||
$docRoot = realpath($argv[2] ?? '.');
|
||||
$entryFile = $argv[3] ?? 'index.php';
|
||||
|
||||
if (!$docRoot || !is_dir($docRoot)) {
|
||||
fwrite(STDERR, "[RouterServer] ERROR: Document root invalid: {$argv[2]}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// ==================== 启动服务器 ====================
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
$server = @stream_socket_server(
|
||||
"tcp://127.0.0.1:$port", $errno, $errstr,
|
||||
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN
|
||||
);
|
||||
if (!$server) {
|
||||
fwrite(STDERR, "[RouterServer] ERROR: Cannot bind to 127.0.0.1:$port: $errstr ($errno)\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
stream_set_blocking($server, true);
|
||||
fwrite(STDERR, "[RouterServer] Listening on 127.0.0.1:$port\n");
|
||||
fwrite(STDERR, "[RouterServer] Document root: $docRoot\n");
|
||||
fwrite(STDERR, "[RouterServer] Entry file: $entryFile\n");
|
||||
|
||||
// 信号处理
|
||||
$running = true;
|
||||
$hasPcntl = function_exists('pcntl_fork');
|
||||
fwrite(STDERR, "[RouterServer] pcntl_fork=" . ($hasPcntl ? 'yes' : 'NO') . "\n");
|
||||
|
||||
if (function_exists('pcntl_signal')) {
|
||||
pcntl_signal(SIGTERM, function () use (&$running) { $running = false; });
|
||||
pcntl_signal(SIGINT, function () use (&$running) { $running = false; });
|
||||
pcntl_signal(SIGCHLD, SIG_IGN); // 自动回收子进程
|
||||
}
|
||||
|
||||
// ==================== Header Function Polyfill ====================
|
||||
// pmmp PHP CLI SAPI 的 header()/headers_list() 不会真正跟踪 HTTP 头。
|
||||
// 通过 disable_functions 禁用内置版本后,在此提供自定义实现,
|
||||
// 将所有 header() 调用记录到全局数组,确保响应包含正确的 HTTP 头。
|
||||
|
||||
if (!function_exists('header')) {
|
||||
$GLOBALS['__router_custom_headers'] = [];
|
||||
|
||||
function header(string $string, bool $replace = true, int $response_code = 0): void {
|
||||
// HTTP 状态行: HTTP/1.1 302 Found
|
||||
if (stripos($string, 'HTTP/') === 0) {
|
||||
if (preg_match('/HTTP\/[\d.]+\s+(\d+)/', $string, $m)) {
|
||||
http_response_code((int)$m[1]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$colonPos = strpos($string, ':');
|
||||
if ($colonPos !== false && $replace) {
|
||||
$headerName = strtolower(trim(substr($string, 0, $colonPos)));
|
||||
// Set-Cookie 不受 replace 影响(多个 Set-Cookie 可共存)
|
||||
if ($headerName !== 'set-cookie') {
|
||||
$GLOBALS['__router_custom_headers'] = array_values(array_filter(
|
||||
$GLOBALS['__router_custom_headers'],
|
||||
function($h) use ($headerName) {
|
||||
$p = strpos($h, ':');
|
||||
return $p === false || strtolower(trim(substr($h, 0, $p))) !== $headerName;
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$GLOBALS['__router_custom_headers'][] = $string;
|
||||
|
||||
// 处理 response_code 参数
|
||||
if ($response_code > 0) {
|
||||
http_response_code($response_code);
|
||||
} elseif ($colonPos !== false) {
|
||||
$headerName = strtolower(trim(substr($string, 0, $colonPos)));
|
||||
if ($headerName === 'location') {
|
||||
$currentCode = http_response_code();
|
||||
if ($currentCode < 300 || $currentCode >= 400) {
|
||||
http_response_code(302);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function headers_list(): array {
|
||||
return $GLOBALS['__router_custom_headers'];
|
||||
}
|
||||
|
||||
function headers_sent(&$filename = null, &$line = null): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
function header_remove(?string $name = null): void {
|
||||
if ($name === null) {
|
||||
$GLOBALS['__router_custom_headers'] = [];
|
||||
} else {
|
||||
$lower = strtolower($name);
|
||||
$GLOBALS['__router_custom_headers'] = array_values(array_filter(
|
||||
$GLOBALS['__router_custom_headers'],
|
||||
function($h) use ($lower) {
|
||||
$p = strpos($h, ':');
|
||||
return $p === false || strtolower(trim(substr($h, 0, $p))) !== $lower;
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
function setcookie(
|
||||
string $name, string $value = '', int|array $expires_or_options = 0,
|
||||
string $path = '', string $domain = '', bool $secure = false, bool $httponly = false
|
||||
): bool {
|
||||
if (is_array($expires_or_options)) {
|
||||
$o = $expires_or_options;
|
||||
$cookie = rawurlencode($name) . '=' . rawurlencode($value);
|
||||
if (!empty($o['expires']) && $o['expires'] > 0) {
|
||||
$cookie .= '; Expires=' . gmdate('D, d M Y H:i:s T', (int)$o['expires']);
|
||||
$cookie .= '; Max-Age=' . max(0, (int)$o['expires'] - time());
|
||||
}
|
||||
if (!empty($o['path'])) $cookie .= '; Path=' . $o['path'];
|
||||
if (!empty($o['domain'])) $cookie .= '; Domain=' . $o['domain'];
|
||||
if (!empty($o['secure'])) $cookie .= '; Secure';
|
||||
if (!empty($o['httponly'])) $cookie .= '; HttpOnly';
|
||||
if (!empty($o['samesite'])) $cookie .= '; SameSite=' . $o['samesite'];
|
||||
} else {
|
||||
$cookie = rawurlencode($name) . '=' . rawurlencode($value);
|
||||
if ($expires_or_options > 0) {
|
||||
$cookie .= '; Expires=' . gmdate('D, d M Y H:i:s T', $expires_or_options);
|
||||
$cookie .= '; Max-Age=' . max(0, $expires_or_options - time());
|
||||
}
|
||||
if ($path !== '') $cookie .= '; Path=' . $path;
|
||||
if ($domain !== '') $cookie .= '; Domain=' . $domain;
|
||||
if ($secure) $cookie .= '; Secure';
|
||||
if ($httponly) $cookie .= '; HttpOnly';
|
||||
}
|
||||
header('Set-Cookie: ' . $cookie, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
function setrawcookie(
|
||||
string $name, string $value = '', int|array $expires_or_options = 0,
|
||||
string $path = '', string $domain = '', bool $secure = false, bool $httponly = false
|
||||
): bool {
|
||||
if (is_array($expires_or_options)) {
|
||||
$o = $expires_or_options;
|
||||
$cookie = $name . '=' . $value;
|
||||
if (!empty($o['expires']) && $o['expires'] > 0) {
|
||||
$cookie .= '; Expires=' . gmdate('D, d M Y H:i:s T', (int)$o['expires']);
|
||||
$cookie .= '; Max-Age=' . max(0, (int)$o['expires'] - time());
|
||||
}
|
||||
if (!empty($o['path'])) $cookie .= '; Path=' . $o['path'];
|
||||
if (!empty($o['domain'])) $cookie .= '; Domain=' . $o['domain'];
|
||||
if (!empty($o['secure'])) $cookie .= '; Secure';
|
||||
if (!empty($o['httponly'])) $cookie .= '; HttpOnly';
|
||||
if (!empty($o['samesite'])) $cookie .= '; SameSite=' . $o['samesite'];
|
||||
} else {
|
||||
$cookie = $name . '=' . $value;
|
||||
if ($expires_or_options > 0) {
|
||||
$cookie .= '; Expires=' . gmdate('D, d M Y H:i:s T', $expires_or_options);
|
||||
$cookie .= '; Max-Age=' . max(0, $expires_or_options - time());
|
||||
}
|
||||
if ($path !== '') $cookie .= '; Path=' . $path;
|
||||
if ($domain !== '') $cookie .= '; Domain=' . $domain;
|
||||
if ($secure) $cookie .= '; Secure';
|
||||
if ($httponly) $cookie .= '; HttpOnly';
|
||||
}
|
||||
header('Set-Cookie: ' . $cookie, false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Session Polyfill ====================
|
||||
// pmmp PHP 没有 session 扩展,提供基于文件的 session 实现
|
||||
// 对所有 PHP 项目自动生效(包括已有项目)
|
||||
|
||||
$GLOBALS['__session_doc_root'] = $docRoot;
|
||||
|
||||
if (!function_exists('session_start')) {
|
||||
$GLOBALS['__session_file'] = null;
|
||||
$GLOBALS['__session_id'] = '';
|
||||
$GLOBALS['__session_started'] = false;
|
||||
$GLOBALS['__session_name'] = 'PHPSESSID';
|
||||
|
||||
function session_start(array $options = []): bool {
|
||||
if ($GLOBALS['__session_started']) return true;
|
||||
|
||||
$docRoot = $GLOBALS['__session_doc_root'] ?? '.';
|
||||
$sessionDir = $docRoot . '/.sessions';
|
||||
if (!is_dir($sessionDir)) @mkdir($sessionDir, 0777, true);
|
||||
|
||||
// 从 cookie 获取 session ID,或生成新的
|
||||
$name = $GLOBALS['__session_name'];
|
||||
$sid = $_COOKIE[$name] ?? '';
|
||||
if (empty($sid) || !preg_match('/^[a-zA-Z0-9,-]{22,256}$/', $sid)) {
|
||||
$sid = bin2hex(random_bytes(16));
|
||||
header("Set-Cookie: $name=$sid; Path=/; HttpOnly; SameSite=Lax");
|
||||
}
|
||||
|
||||
$GLOBALS['__session_id'] = $sid;
|
||||
$GLOBALS['__session_file'] = $sessionDir . '/sess_' . $sid;
|
||||
|
||||
$_SESSION = [];
|
||||
if (file_exists($GLOBALS['__session_file'])) {
|
||||
$data = @file_get_contents($GLOBALS['__session_file']);
|
||||
if ($data !== false) {
|
||||
$decoded = @json_decode($data, true);
|
||||
if (is_array($decoded)) $_SESSION = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
$GLOBALS['__session_started'] = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function session_write_close(): bool {
|
||||
if (!$GLOBALS['__session_started']) return true;
|
||||
if ($GLOBALS['__session_file']) {
|
||||
@file_put_contents(
|
||||
$GLOBALS['__session_file'],
|
||||
json_encode($_SESSION ?? [], JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
}
|
||||
$GLOBALS['__session_started'] = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function session_commit(): bool {
|
||||
return session_write_close();
|
||||
}
|
||||
|
||||
function session_destroy(): bool {
|
||||
$_SESSION = [];
|
||||
if ($GLOBALS['__session_file'] && file_exists($GLOBALS['__session_file'])) {
|
||||
@unlink($GLOBALS['__session_file']);
|
||||
}
|
||||
$GLOBALS['__session_started'] = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function session_id(string $id = ''): string|false {
|
||||
if ($id !== '') $GLOBALS['__session_id'] = $id;
|
||||
return $GLOBALS['__session_id'];
|
||||
}
|
||||
|
||||
function session_name(string $name = ''): string|false {
|
||||
$old = $GLOBALS['__session_name'];
|
||||
if ($name !== '') $GLOBALS['__session_name'] = $name;
|
||||
return $old;
|
||||
}
|
||||
|
||||
function session_status(): int {
|
||||
// PHP_SESSION_ACTIVE = 2, PHP_SESSION_NONE = 1
|
||||
return $GLOBALS['__session_started'] ? 2 : 1;
|
||||
}
|
||||
|
||||
function session_regenerate_id(bool $delete_old = false): bool {
|
||||
if (!$GLOBALS['__session_started']) return false;
|
||||
$oldFile = $GLOBALS['__session_file'];
|
||||
$sid = bin2hex(random_bytes(16));
|
||||
$GLOBALS['__session_id'] = $sid;
|
||||
$name = $GLOBALS['__session_name'];
|
||||
header("Set-Cookie: $name=$sid; Path=/; HttpOnly; SameSite=Lax");
|
||||
$docRoot = $GLOBALS['__session_doc_root'] ?? '.';
|
||||
$GLOBALS['__session_file'] = $docRoot . '/.sessions/sess_' . $sid;
|
||||
if ($delete_old && $oldFile && file_exists($oldFile)) @unlink($oldFile);
|
||||
return true;
|
||||
}
|
||||
|
||||
function session_unset(): bool {
|
||||
$_SESSION = [];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 主循环 ====================
|
||||
while ($running) {
|
||||
if (function_exists('pcntl_signal_dispatch')) {
|
||||
pcntl_signal_dispatch();
|
||||
}
|
||||
|
||||
$read = [$server]; $write = $except = null;
|
||||
$changed = @stream_select($read, $write, $except, 1);
|
||||
if ($changed === false || $changed === 0) continue;
|
||||
|
||||
$client = @stream_socket_accept($server, 0);
|
||||
if (!$client) continue;
|
||||
|
||||
$request = readHttpRequest($client);
|
||||
if (!$request) {
|
||||
fwrite(STDERR, "[RouterServer] Empty request, closing\n");
|
||||
fclose($client);
|
||||
continue;
|
||||
}
|
||||
|
||||
fwrite(STDERR, "[RouterServer] {$request['method']} {$request['path']}\n");
|
||||
|
||||
// 内置健康检查端点 — 不执行任何 PHP 文件
|
||||
if ($request['path'] === '/__health') {
|
||||
sendRawResponse($client, 200, 'OK', ['Content-Type: text/plain'], 'OK');
|
||||
fclose($client);
|
||||
continue;
|
||||
}
|
||||
|
||||
$filePath = resolveFilePath($request['path'], $docRoot, $entryFile);
|
||||
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
||||
|
||||
// 静态文件 — 主进程直接响应
|
||||
if ($ext !== 'php' && file_exists($filePath) && !is_dir($filePath)) {
|
||||
serveStaticFile($client, $filePath);
|
||||
fclose($client);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!file_exists($filePath)) {
|
||||
sendRawResponse($client, 404, 'Not Found', [], 'Not Found');
|
||||
fclose($client);
|
||||
continue;
|
||||
}
|
||||
|
||||
// PHP 文件 — fork 子进程隔离执行
|
||||
if ($hasPcntl) {
|
||||
$pid = pcntl_fork();
|
||||
if ($pid === -1) {
|
||||
executePHP($client, $filePath, $request, $docRoot, $port);
|
||||
fclose($client);
|
||||
} elseif ($pid === 0) {
|
||||
fclose($server);
|
||||
executePHP($client, $filePath, $request, $docRoot, $port);
|
||||
fclose($client);
|
||||
exit(0);
|
||||
} else {
|
||||
fclose($client);
|
||||
}
|
||||
} else {
|
||||
executePHP($client, $filePath, $request, $docRoot, $port);
|
||||
fclose($client);
|
||||
}
|
||||
}
|
||||
|
||||
fclose($server);
|
||||
fwrite(STDERR, "[RouterServer] Stopped\n");
|
||||
exit(0);
|
||||
|
||||
// ==================== HTTP 请求解析 ====================
|
||||
|
||||
function readHttpRequest($client): ?array {
|
||||
stream_set_timeout($client, 10);
|
||||
|
||||
$requestLine = fgets($client, 8192);
|
||||
if (!$requestLine) return null;
|
||||
if (!preg_match('/^(\w+)\s+(\S+)\s+HTTP\/([\d.]+)/', trim($requestLine), $m)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$method = strtoupper($m[1]);
|
||||
$uri = $m[2];
|
||||
|
||||
$headers = [];
|
||||
while (($line = fgets($client, 8192)) !== false) {
|
||||
$line = rtrim($line, "\r\n");
|
||||
if ($line === '') break;
|
||||
if (($pos = strpos($line, ':')) !== false) {
|
||||
$headers[strtolower(trim(substr($line, 0, $pos)))] = trim(substr($line, $pos + 1));
|
||||
}
|
||||
}
|
||||
|
||||
$body = '';
|
||||
$contentLength = (int)($headers['content-length'] ?? 0);
|
||||
if ($contentLength > 0) {
|
||||
$remaining = $contentLength;
|
||||
while ($remaining > 0) {
|
||||
$chunk = fread($client, min($remaining, 65536));
|
||||
if ($chunk === false || $chunk === '') break;
|
||||
$body .= $chunk;
|
||||
$remaining -= strlen($chunk);
|
||||
}
|
||||
}
|
||||
|
||||
$parts = parse_url($uri);
|
||||
$path = urldecode($parts['path'] ?? '/');
|
||||
$queryString = $parts['query'] ?? '';
|
||||
|
||||
return compact('method', 'uri', 'path', 'queryString', 'headers', 'body');
|
||||
}
|
||||
|
||||
// ==================== 路径解析 ====================
|
||||
|
||||
function resolveFilePath(string $path, string $docRoot, string $entryFile): string {
|
||||
$path = '/' . ltrim(str_replace('\\', '/', $path), '/');
|
||||
if (strpos($path, '..') !== false) return '';
|
||||
|
||||
$filePath = $docRoot . $path;
|
||||
$requestedExt = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
|
||||
if (is_dir($filePath)) {
|
||||
foreach (['index.php', 'index.html', 'index.htm'] as $idx) {
|
||||
$c = $filePath . '/' . $idx;
|
||||
if (file_exists($c)) return $c;
|
||||
}
|
||||
}
|
||||
|
||||
if (file_exists($filePath) && !is_dir($filePath)) return $filePath;
|
||||
|
||||
if ($requestedExt !== '' && in_array($requestedExt, [
|
||||
'css','js','mjs','json','xml','txt','csv','png','jpg','jpeg','gif','svg','ico',
|
||||
'webp','avif','woff','woff2','ttf','otf','pdf','zip','mp4','webm','mp3','ogg','wav',
|
||||
'map','wasm'
|
||||
], true)) {
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
return $docRoot . '/' . $entryFile;
|
||||
}
|
||||
|
||||
// ==================== PHP 执行 ====================
|
||||
|
||||
function executePHP($client, string $filePath, array $request, string $docRoot, int $port): void {
|
||||
$method = $request['method'];
|
||||
$uri = $request['uri'];
|
||||
$queryString = $request['queryString'];
|
||||
$headers = $request['headers'];
|
||||
$body = $request['body'];
|
||||
$path = $request['path'];
|
||||
|
||||
// 标记响应是否已发送,防止 shutdown function 重复发送
|
||||
$GLOBALS['__router_response_sent'] = false;
|
||||
|
||||
// 将 body 写入临时文件并重定向 stdin (fd 0)
|
||||
// 这样 php://input 可以正确读取请求体
|
||||
$tmpInput = null;
|
||||
if (strlen($body) > 0) {
|
||||
$tmpInput = tempnam(sys_get_temp_dir(), 'phpinput_');
|
||||
file_put_contents($tmpInput, $body);
|
||||
fclose(STDIN);
|
||||
$GLOBALS['STDIN'] = fopen($tmpInput, 'r'); // 获得 fd 0
|
||||
}
|
||||
|
||||
// 构建 $_SERVER
|
||||
$_SERVER = [
|
||||
'REQUEST_METHOD' => $method,
|
||||
'REQUEST_URI' => $uri,
|
||||
'QUERY_STRING' => $queryString,
|
||||
'SCRIPT_FILENAME' => $filePath,
|
||||
'SCRIPT_NAME' => $path,
|
||||
'PHP_SELF' => $path,
|
||||
'DOCUMENT_ROOT' => $docRoot,
|
||||
'SERVER_NAME' => '*********',
|
||||
'SERVER_PORT' => (string)$port,
|
||||
'SERVER_PROTOCOL' => 'HTTP/1.1',
|
||||
'SERVER_SOFTWARE' => 'PHPRouterServer/1.0',
|
||||
'GATEWAY_INTERFACE' => 'CGI/1.1',
|
||||
'REDIRECT_STATUS' => '200',
|
||||
'REMOTE_ADDR' => '*********',
|
||||
'REMOTE_PORT' => '0',
|
||||
'HTTP_HOST' => $headers['host'] ?? "*********:$port",
|
||||
'CONTENT_TYPE' => $headers['content-type'] ?? '',
|
||||
'CONTENT_LENGTH' => (string)strlen($body),
|
||||
'REQUEST_TIME' => time(),
|
||||
'REQUEST_TIME_FLOAT'=> microtime(true),
|
||||
];
|
||||
foreach ($headers as $k => $v) {
|
||||
$_SERVER['HTTP_' . strtoupper(str_replace('-', '_', $k))] = $v;
|
||||
}
|
||||
|
||||
// $_GET
|
||||
$_GET = [];
|
||||
parse_str($queryString, $_GET);
|
||||
|
||||
// $_POST & $_FILES
|
||||
$_POST = [];
|
||||
$_FILES = [];
|
||||
$ct = $headers['content-type'] ?? '';
|
||||
if (in_array($method, ['POST', 'PUT', 'PATCH']) && $body !== '') {
|
||||
if (stripos($ct, 'application/x-www-form-urlencoded') !== false) {
|
||||
parse_str($body, $_POST);
|
||||
} elseif (stripos($ct, 'multipart/form-data') !== false) {
|
||||
parseMultipart($ct, $body, $_POST, $_FILES);
|
||||
} elseif (stripos($ct, 'application/json') !== false) {
|
||||
// JSON body → $_POST fallback(当 php://input 在 CLI SAPI 下不可用时)
|
||||
$decoded = json_decode($body, true);
|
||||
if (is_array($decoded)) $_POST = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
// $_COOKIE
|
||||
$_COOKIE = [];
|
||||
if (isset($headers['cookie'])) {
|
||||
foreach (explode(';', $headers['cookie']) as $c) {
|
||||
$c = trim($c);
|
||||
if (($eq = strpos($c, '=')) !== false) {
|
||||
$_COOKIE[trim(substr($c, 0, $eq))] = urldecode(trim(substr($c, $eq + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$_REQUEST = array_merge($_GET, $_POST, $_COOKIE);
|
||||
|
||||
// 注册 shutdown function — 安全网:当脚本调用 exit() 或发生致命错误时发送响应
|
||||
$responseClient = $client;
|
||||
register_shutdown_function(function () use ($responseClient, $tmpInput, $uri) {
|
||||
// 如果响应已发送(正常路径),仅清理
|
||||
if (!empty($GLOBALS['__router_response_sent'])) {
|
||||
if ($tmpInput && file_exists($tmpInput)) @unlink($tmpInput);
|
||||
return;
|
||||
}
|
||||
$GLOBALS['__router_response_sent'] = true;
|
||||
|
||||
$output = '';
|
||||
while (ob_get_level() > 0) {
|
||||
$output = ob_get_clean() . $output;
|
||||
}
|
||||
$code = http_response_code() ?: 200;
|
||||
$headerList = headers_list();
|
||||
$lastError = error_get_last();
|
||||
if ($lastError && in_array($lastError['type'] ?? 0, [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR], true)) {
|
||||
$code = 500;
|
||||
$message = ($lastError['message'] ?? 'Unknown fatal error')
|
||||
. ' in ' . ($lastError['file'] ?? 'unknown')
|
||||
. ':' . ($lastError['line'] ?? 0);
|
||||
fwrite(STDERR, "[RouterServer] Fatal error: $message\n");
|
||||
if ($output === '') {
|
||||
$output = '<h1>500 Internal Server Error</h1><pre>' . htmlspecialchars($message) . '</pre>';
|
||||
}
|
||||
}
|
||||
// Surface the router's own diagnostic (fatal message / trace) to the WebView
|
||||
// layer: local error pages read this header to show the cause instead of a
|
||||
// bare "HTTP 500" that hides whether the failure is a fatal, a thrown
|
||||
// exception, or PHP output. Keep it bounded — response bodies can be large.
|
||||
if ($code >= 500 && $output !== '' && strlen($output) <= 65536) {
|
||||
$headerList[] = 'X-Wta-Router-Error: ' . base64_encode($output);
|
||||
}
|
||||
if (function_exists('header_remove')) header_remove();
|
||||
|
||||
fwrite(STDERR, "[RouterServer] Shutdown response uri=$uri code=$code bytes=" . strlen($output) . " " . describeHeaders($headerList) . "\n");
|
||||
sendRawResponse($responseClient, $code, httpStatusText($code), $headerList, $output);
|
||||
@fclose($responseClient);
|
||||
|
||||
if ($tmpInput && file_exists($tmpInput)) @unlink($tmpInput);
|
||||
});
|
||||
|
||||
ob_start();
|
||||
http_response_code(200);
|
||||
if (function_exists('header_remove')) header_remove();
|
||||
|
||||
// 设置全局变量,让 PHP 脚本可以通过 $GLOBALS['__HTTP_RAW_BODY'] 获取原始请求体
|
||||
// 这是 php://input 在 CLI SAPI + fork 模式下的可靠替代方案
|
||||
$GLOBALS['__HTTP_RAW_BODY'] = $body;
|
||||
|
||||
chdir($docRoot);
|
||||
|
||||
try {
|
||||
include $filePath;
|
||||
} catch (\Throwable $e) {
|
||||
while (ob_get_level() > 1) ob_end_clean();
|
||||
http_response_code(500);
|
||||
fwrite(STDERR, "[RouterServer] PHP error: " . $e->getMessage() . "\n");
|
||||
echo "<h1>500 Internal Server Error</h1><pre>"
|
||||
. htmlspecialchars($e->getMessage()) . "\n"
|
||||
. htmlspecialchars($e->getTraceAsString()) . "</pre>";
|
||||
}
|
||||
|
||||
// Thrown-exception path: response goes through the normal-path sender below,
|
||||
// which also stamps X-Wta-Router-Error on 5xx (see the shutdown handler).
|
||||
|
||||
// 自动保存 session 数据(模拟 PHP 原生 session 行为)
|
||||
if (function_exists('session_write_close') && ($GLOBALS['__session_started'] ?? false)) {
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
// 正常路径:显式发送响应(在 fclose 之前!)
|
||||
if (!$GLOBALS['__router_response_sent']) {
|
||||
$GLOBALS['__router_response_sent'] = true;
|
||||
$output = '';
|
||||
while (ob_get_level() > 0) {
|
||||
$output = ob_get_clean() . $output;
|
||||
}
|
||||
$code = http_response_code() ?: 200;
|
||||
$headerList = headers_list();
|
||||
// Same 5xx diagnostic stamping as the shutdown handler (see there).
|
||||
if ($code >= 500 && $output !== '' && strlen($output) <= 65536) {
|
||||
$headerList[] = 'X-Wta-Router-Error: ' . base64_encode($output);
|
||||
}
|
||||
if (function_exists('header_remove')) header_remove();
|
||||
|
||||
fwrite(STDERR, "[RouterServer] Response uri=$uri code=$code bytes=" . strlen($output) . " " . describeHeaders($headerList) . "\n");
|
||||
sendRawResponse($client, $code, httpStatusText($code), $headerList, $output);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Multipart 解析 ====================
|
||||
|
||||
function parseMultipart(string $contentType, string $body, array &$post, array &$files): void {
|
||||
if (!preg_match('/boundary=(.+?)(?:;|$)/i', $contentType, $m)) return;
|
||||
$boundary = trim($m[1], '"');
|
||||
$parts = explode("--$boundary", $body);
|
||||
array_shift($parts);
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$part = ltrim($part, "\r\n");
|
||||
if (str_starts_with(trim($part), '--') || empty(trim($part))) continue;
|
||||
|
||||
$headerEnd = strpos($part, "\r\n\r\n");
|
||||
if ($headerEnd === false) continue;
|
||||
|
||||
$ph = substr($part, 0, $headerEnd);
|
||||
$pb = rtrim(substr($part, $headerEnd + 4), "\r\n");
|
||||
|
||||
if (!preg_match('/name="([^"]+)"/', $ph, $nm)) continue;
|
||||
$name = $nm[1];
|
||||
|
||||
if (preg_match('/filename="([^"]*)"/', $ph, $fn) && $fn[1] !== '') {
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'php_upload_');
|
||||
file_put_contents($tmp, $pb);
|
||||
$pct = 'application/octet-stream';
|
||||
if (preg_match('/Content-Type:\s*(.+)/i', $ph, $ctm)) $pct = trim($ctm[1]);
|
||||
$files[$name] = ['name'=>$fn[1],'type'=>$pct,'tmp_name'=>$tmp,'error'=>UPLOAD_ERR_OK,'size'=>strlen($pb)];
|
||||
} else {
|
||||
$post[$name] = $pb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 响应发送 ====================
|
||||
|
||||
function sendRawResponse($client, int $code, string $status, array $headers, string $body): void {
|
||||
$resp = "HTTP/1.1 $code $status\r\n";
|
||||
$hasCT = false; $hasCL = false;
|
||||
foreach ($headers as $h) {
|
||||
$resp .= "$h\r\n";
|
||||
if (stripos($h, 'content-type:') === 0) $hasCT = true;
|
||||
if (stripos($h, 'content-length:') === 0) $hasCL = true;
|
||||
}
|
||||
if (!$hasCT) $resp .= "Content-Type: text/html; charset=UTF-8\r\n";
|
||||
if (!$hasCL) $resp .= "Content-Length: " . strlen($body) . "\r\n";
|
||||
$resp .= "Connection: close\r\n\r\n" . $body;
|
||||
@fwrite($client, $resp);
|
||||
}
|
||||
|
||||
function describeHeaders(array $headers): string {
|
||||
$parts = [];
|
||||
foreach ($headers as $h) {
|
||||
if (stripos($h, 'location:') === 0 || stripos($h, 'content-type:') === 0) {
|
||||
$parts[] = $h;
|
||||
} elseif (stripos($h, 'set-cookie:') === 0) {
|
||||
$parts[] = 'Set-Cookie: <redacted>';
|
||||
}
|
||||
}
|
||||
return empty($parts) ? 'headers=[]' : 'headers=[' . implode('; ', $parts) . ']';
|
||||
}
|
||||
|
||||
// ==================== 静态文件服务 ====================
|
||||
|
||||
function serveStaticFile($client, string $filePath): void {
|
||||
$mime = getMimeType($filePath);
|
||||
$size = filesize($filePath);
|
||||
$resp = "HTTP/1.1 200 OK\r\nContent-Type: $mime\r\nContent-Length: $size\r\n"
|
||||
. "Connection: close\r\nCache-Control: public, max-age=3600\r\n\r\n";
|
||||
fwrite($client, $resp);
|
||||
$fp = fopen($filePath, 'rb');
|
||||
if ($fp) {
|
||||
while (!feof($fp)) { @fwrite($client, fread($fp, 65536)); }
|
||||
fclose($fp);
|
||||
}
|
||||
}
|
||||
|
||||
function getMimeType(string $f): string {
|
||||
static $t = [
|
||||
'html'=>'text/html','htm'=>'text/html','css'=>'text/css',
|
||||
'js'=>'application/javascript','mjs'=>'application/javascript',
|
||||
'json'=>'application/json','xml'=>'application/xml',
|
||||
'txt'=>'text/plain','csv'=>'text/csv',
|
||||
'png'=>'image/png','jpg'=>'image/jpeg','jpeg'=>'image/jpeg',
|
||||
'gif'=>'image/gif','svg'=>'image/svg+xml','ico'=>'image/x-icon',
|
||||
'webp'=>'image/webp','avif'=>'image/avif',
|
||||
'woff'=>'font/woff','woff2'=>'font/woff2','ttf'=>'font/ttf','otf'=>'font/otf',
|
||||
'pdf'=>'application/pdf','zip'=>'application/zip',
|
||||
'mp4'=>'video/mp4','webm'=>'video/webm',
|
||||
'mp3'=>'audio/mpeg','ogg'=>'audio/ogg','wav'=>'audio/wav',
|
||||
'map'=>'application/json','wasm'=>'application/wasm',
|
||||
];
|
||||
return $t[strtolower(pathinfo($f, PATHINFO_EXTENSION))] ?? 'application/octet-stream';
|
||||
}
|
||||
|
||||
function httpStatusText(int $c): string {
|
||||
static $t = [
|
||||
200=>'OK',201=>'Created',204=>'No Content',
|
||||
301=>'Moved Permanently',302=>'Found',304=>'Not Modified',
|
||||
307=>'Temporary Redirect',308=>'Permanent Redirect',
|
||||
400=>'Bad Request',401=>'Unauthorized',403=>'Forbidden',
|
||||
404=>'Not Found',405=>'Method Not Allowed',422=>'Unprocessable Entity',
|
||||
500=>'Internal Server Error',502=>'Bad Gateway',503=>'Service Unavailable',
|
||||
];
|
||||
return $t[$c] ?? 'Unknown';
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
{}
|
||||
328
app/src/main/assets/sample_projects/docs-hexo-ar/index.html
Normal file
328
app/src/main/assets/sample_projects/docs-hexo-ar/index.html
Normal file
@ -0,0 +1,328 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>وثائق Hexo</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #0a0a1a;
|
||||
--card: rgba(255, 255, 255, 0.04);
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--text: #f0f0ff;
|
||||
--text2: rgba(240, 240, 255, 0.55);
|
||||
--text3: rgba(240, 240, 255, 0.3);
|
||||
--blue: #0ea5e9;
|
||||
--radius: 14px
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Tahoma, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
direction: rtl
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: -40%;
|
||||
left: -40%;
|
||||
width: 180%;
|
||||
height: 180%;
|
||||
background: radial-gradient(ellipse at 50% 20%, rgba(14, 165, 233, 0.06) 0%, transparent 55%);
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
.c {
|
||||
max-width: 460px;
|
||||
margin: 0 auto;
|
||||
padding: 0 14px 32px;
|
||||
position: relative;
|
||||
z-index: 1
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 40px 0 32px
|
||||
}
|
||||
|
||||
.hero-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
animation: float 3s ease-in-out infinite
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0)
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-8px)
|
||||
}
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #0ea5e9, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 8px
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 14px;
|
||||
color: var(--text2);
|
||||
line-height: 1.6;
|
||||
max-width: 320px;
|
||||
margin: 0 auto
|
||||
}
|
||||
|
||||
.badge-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text2)
|
||||
}
|
||||
|
||||
.badge.active {
|
||||
background: rgba(14, 165, 233, 0.1);
|
||||
color: var(--blue)
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
cursor: pointer;
|
||||
transition: all .2s
|
||||
}
|
||||
|
||||
.card:active {
|
||||
transform: scale(.97)
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text2);
|
||||
line-height: 1.5
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
margin-bottom: 14px
|
||||
}
|
||||
|
||||
.section-h {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 14px
|
||||
}
|
||||
|
||||
.code {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #38bdf8;
|
||||
margin: 8px 0;
|
||||
direction: ltr;
|
||||
text-align: left
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03)
|
||||
}
|
||||
|
||||
.step:last-child {
|
||||
border: none
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: rgba(14, 165, 233, 0.1);
|
||||
color: var(--blue);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.step-info {
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px
|
||||
}
|
||||
|
||||
.ft {
|
||||
text-align: center;
|
||||
padding: 16px
|
||||
}
|
||||
|
||||
.ft-b {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text3);
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
background: rgba(14, 165, 233, 0.04);
|
||||
border: 1px solid rgba(14, 165, 233, 0.08)
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px)
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0)
|
||||
}
|
||||
}
|
||||
|
||||
.card,
|
||||
.section {
|
||||
animation: fadeIn .5s ease backwards
|
||||
}
|
||||
|
||||
.card:nth-child(1) {
|
||||
animation-delay: .1s
|
||||
}
|
||||
|
||||
.card:nth-child(2) {
|
||||
animation-delay: .15s
|
||||
}
|
||||
|
||||
.card:nth-child(3) {
|
||||
animation-delay: .2s
|
||||
}
|
||||
|
||||
.card:nth-child(4) {
|
||||
animation-delay: .25s
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="c">
|
||||
<div class="hero">
|
||||
<div class="hero-icon">📖</div>
|
||||
<h1>وثائق Hexo</h1>
|
||||
<p>إطار عمل مدونات سريع وبسيط مبني على Node.js</p>
|
||||
<div class="badge-row"><span class="badge active">v7.0</span><span class="badge">Node.js</span><span
|
||||
class="badge">Markdown</span></div>
|
||||
</div>
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<div class="card-icon">🚀</div>
|
||||
<div class="card-title">البداية السريعة</div>
|
||||
<div class="card-desc">أنشئ مدونتك في دقائق</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🔧</div>
|
||||
<div class="card-title">الإعدادات</div>
|
||||
<div class="card-desc">خصص كل التفاصيل</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🎨</div>
|
||||
<div class="card-title">السمات</div>
|
||||
<div class="card-desc">مئات السمات الجميلة</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🔌</div>
|
||||
<div class="card-title">الإضافات</div>
|
||||
<div class="card-desc">نظام إضافات غني</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" style="animation-delay:.3s">
|
||||
<div class="section-h">⚡ البداية السريعة</div>
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">تثبيت Hexo</div>
|
||||
<div class="code">npm install -g hexo-cli</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">إنشاء المشروع</div>
|
||||
<div class="code">hexo init my-blog && cd my-blog</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">تشغيل الخادم</div>
|
||||
<div class="code">hexo server</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ft">
|
||||
<div class="ft-b">📖 وثائق Hexo · مقدم من WebToApp</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
14
app/src/main/assets/sample_projects/docs-hexo-ar/style.css
Normal file
14
app/src/main/assets/sample_projects/docs-hexo-ar/style.css
Normal file
@ -0,0 +1,14 @@
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;display:flex;min-height:100vh;background:#fafafa}
|
||||
.sidebar{width:240px;background:#fff;border-right:1px solid #eee;padding:20px}
|
||||
.logo{font-size:20px;font-weight:700;margin-bottom:24px;padding:8px 0}
|
||||
.sidebar a{display:block;padding:10px 16px;border-radius:8px;color:#333;text-decoration:none;margin-bottom:4px;font-size:14px}
|
||||
.sidebar a.active{background:#e8f5e9;color:#2e7d32;font-weight:600}
|
||||
.sidebar a:hover{background:#f5f5f5}
|
||||
.content{flex:1;padding:40px 60px;max-width:800px}
|
||||
h1{font-size:32px;color:#1a1a1a;margin-bottom:16px}
|
||||
p{color:#666;line-height:1.8;margin-bottom:24px}
|
||||
.features{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:32px}
|
||||
.feature{background:#fff;border:1px solid #eee;border-radius:12px;padding:20px}
|
||||
.feature h3{font-size:16px;margin-bottom:8px}
|
||||
.feature p{font-size:14px;color:#888;margin:0}
|
||||
@ -0,0 +1 @@
|
||||
{}
|
||||
385
app/src/main/assets/sample_projects/docs-hexo-en/index.html
Normal file
385
app/src/main/assets/sample_projects/docs-hexo-en/index.html
Normal file
@ -0,0 +1,385 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Hexo Docs</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #0a0a1a;
|
||||
--card: rgba(255, 255, 255, 0.04);
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--text: #f0f0ff;
|
||||
--text2: rgba(240, 240, 255, 0.55);
|
||||
--text3: rgba(240, 240, 255, 0.3);
|
||||
--blue: #0ea5e9;
|
||||
--radius: 14px
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: -40%;
|
||||
left: -40%;
|
||||
width: 180%;
|
||||
height: 180%;
|
||||
background: radial-gradient(ellipse at 50% 20%, rgba(14, 165, 233, 0.06) 0%, transparent 55%);
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
.c {
|
||||
max-width: 460px;
|
||||
margin: 0 auto;
|
||||
padding: 0 14px 32px;
|
||||
position: relative;
|
||||
z-index: 1
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 40px 0 32px
|
||||
}
|
||||
|
||||
.hero-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
animation: float 3s ease-in-out infinite
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0)
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-8px)
|
||||
}
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #0ea5e9, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 8px
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 14px;
|
||||
color: var(--text2);
|
||||
line-height: 1.6;
|
||||
max-width: 320px;
|
||||
margin: 0 auto
|
||||
}
|
||||
|
||||
.badge-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text2)
|
||||
}
|
||||
|
||||
.badge.active {
|
||||
background: rgba(14, 165, 233, 0.1);
|
||||
color: var(--blue);
|
||||
border-color: rgba(14, 165, 233, 0.2)
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
cursor: pointer;
|
||||
transition: all .2s
|
||||
}
|
||||
|
||||
.card:active {
|
||||
transform: scale(.97)
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text2);
|
||||
line-height: 1.5
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
margin-bottom: 14px
|
||||
}
|
||||
|
||||
.section-h {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px
|
||||
}
|
||||
|
||||
.code {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #38bdf8;
|
||||
margin: 8px 0
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03)
|
||||
}
|
||||
|
||||
.step:last-child {
|
||||
border: none
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: rgba(14, 165, 233, 0.1);
|
||||
color: var(--blue);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.step-info {
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px
|
||||
}
|
||||
|
||||
.nav-list a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
|
||||
text-decoration: none;
|
||||
color: var(--text)
|
||||
}
|
||||
|
||||
.nav-list a:last-child {
|
||||
border: none
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 13px;
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
.nav-arrow {
|
||||
margin-left: auto;
|
||||
color: var(--text3)
|
||||
}
|
||||
|
||||
.ft {
|
||||
text-align: center;
|
||||
padding: 16px
|
||||
}
|
||||
|
||||
.ft-b {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text3);
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
background: rgba(14, 165, 233, 0.04);
|
||||
border: 1px solid rgba(14, 165, 233, 0.08)
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px)
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0)
|
||||
}
|
||||
}
|
||||
|
||||
.card,
|
||||
.section {
|
||||
animation: fadeIn .5s ease backwards
|
||||
}
|
||||
|
||||
.card:nth-child(1) {
|
||||
animation-delay: .1s
|
||||
}
|
||||
|
||||
.card:nth-child(2) {
|
||||
animation-delay: .15s
|
||||
}
|
||||
|
||||
.card:nth-child(3) {
|
||||
animation-delay: .2s
|
||||
}
|
||||
|
||||
.card:nth-child(4) {
|
||||
animation-delay: .25s
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="c">
|
||||
<div class="hero">
|
||||
<div class="hero-icon">📖</div>
|
||||
<h1>Hexo Docs</h1>
|
||||
<p>A fast, simple & powerful blog framework built on Node.js</p>
|
||||
<div class="badge-row"><span class="badge active">v7.0</span><span class="badge">Node.js</span><span
|
||||
class="badge">Markdown</span><span class="badge">Open Source</span></div>
|
||||
</div>
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<div class="card-icon">🚀</div>
|
||||
<div class="card-title">Quick Start</div>
|
||||
<div class="card-desc">Create your first blog in minutes</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🔧</div>
|
||||
<div class="card-title">Configuration</div>
|
||||
<div class="card-desc">Customize every detail</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🎨</div>
|
||||
<div class="card-title">Themes</div>
|
||||
<div class="card-desc">Hundreds of beautiful themes</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🔌</div>
|
||||
<div class="card-title">Plugins</div>
|
||||
<div class="card-desc">Rich plugin ecosystem</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" style="animation-delay:.3s">
|
||||
<div class="section-h">⚡ Getting Started</div>
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">Install Hexo</div>
|
||||
<div class="code">npm install -g hexo-cli</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">Initialize</div>
|
||||
<div class="code">hexo init my-blog && cd my-blog</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">Start Server</div>
|
||||
<div class="code">hexo server</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" style="animation-delay:.35s">
|
||||
<div class="section-h">📚 Navigation</div>
|
||||
<div class="nav-list">
|
||||
<a href="#">
|
||||
<div class="nav-icon" style="background:rgba(14,165,233,0.1)">📝</div><span
|
||||
class="nav-text">Writing</span><span class="nav-arrow">→</span>
|
||||
</a>
|
||||
<a href="#">
|
||||
<div class="nav-icon" style="background:rgba(139,92,246,0.1)">🏷️</div><span class="nav-text">Tags &
|
||||
Categories</span><span class="nav-arrow">→</span>
|
||||
</a>
|
||||
<a href="#">
|
||||
<div class="nav-icon" style="background:rgba(16,185,129,0.1)">🌐</div><span
|
||||
class="nav-text">Deployment</span><span class="nav-arrow">→</span>
|
||||
</a>
|
||||
<a href="#">
|
||||
<div class="nav-icon" style="background:rgba(245,158,11,0.1)">⚙️</div><span
|
||||
class="nav-text">Advanced Config</span><span class="nav-arrow">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ft">
|
||||
<div class="ft-b">📖 Hexo Docs · Powered by WebToApp</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
14
app/src/main/assets/sample_projects/docs-hexo-en/style.css
Normal file
14
app/src/main/assets/sample_projects/docs-hexo-en/style.css
Normal file
@ -0,0 +1,14 @@
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;display:flex;min-height:100vh;background:#fafafa}
|
||||
.sidebar{width:240px;background:#fff;border-right:1px solid #eee;padding:20px}
|
||||
.logo{font-size:20px;font-weight:700;margin-bottom:24px;padding:8px 0}
|
||||
.sidebar a{display:block;padding:10px 16px;border-radius:8px;color:#333;text-decoration:none;margin-bottom:4px;font-size:14px}
|
||||
.sidebar a.active{background:#e8f5e9;color:#2e7d32;font-weight:600}
|
||||
.sidebar a:hover{background:#f5f5f5}
|
||||
.content{flex:1;padding:40px 60px;max-width:800px}
|
||||
h1{font-size:32px;color:#1a1a1a;margin-bottom:16px}
|
||||
p{color:#666;line-height:1.8;margin-bottom:24px}
|
||||
.features{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:32px}
|
||||
.feature{background:#fff;border:1px solid #eee;border-radius:12px;padding:20px}
|
||||
.feature h3{font-size:16px;margin-bottom:8px}
|
||||
.feature p{font-size:14px;color:#888;margin:0}
|
||||
@ -0,0 +1 @@
|
||||
{}
|
||||
87
app/src/main/assets/sample_projects/docs-hexo/index.html
Normal file
87
app/src/main/assets/sample_projects/docs-hexo/index.html
Normal file
@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Hexo 文档</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{--bg:#0a0a1a;--card:rgba(255,255,255,0.04);--border:rgba(255,255,255,0.07);--text:#f0f0ff;--text2:rgba(240,240,255,0.55);--text3:rgba(240,240,255,0.3);--blue:#0ea5e9;--radius:14px}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;-webkit-font-smoothing:antialiased}
|
||||
body::before{content:'';position:fixed;top:-40%;left:-40%;width:180%;height:180%;background:radial-gradient(ellipse at 50% 20%,rgba(14,165,233,0.06) 0%,transparent 55%);pointer-events:none}
|
||||
.c{max-width:460px;margin:0 auto;padding:0 14px 32px;position:relative;z-index:1}
|
||||
.hero{text-align:center;padding:40px 0 32px}
|
||||
.hero-icon{font-size:48px;margin-bottom:12px;animation:float 3s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
|
||||
.hero h1{font-size:28px;font-weight:800;background:linear-gradient(135deg,#0ea5e9,#38bdf8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:8px}
|
||||
.hero p{font-size:14px;color:var(--text2);line-height:1.6;max-width:320px;margin:0 auto}
|
||||
.badge-row{display:flex;justify-content:center;gap:8px;margin-top:16px;flex-wrap:wrap}
|
||||
.badge{font-size:11px;font-weight:600;padding:4px 12px;border-radius:20px;background:var(--card);border:1px solid var(--border);color:var(--text2)}
|
||||
.badge.active{background:rgba(14,165,233,0.1);color:var(--blue);border-color:rgba(14,165,233,0.2)}
|
||||
.cards{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px}
|
||||
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;backdrop-filter:blur(16px);transition:all .2s;cursor:pointer}
|
||||
.card:active{transform:scale(.97);background:rgba(255,255,255,0.06)}
|
||||
.card-icon{font-size:24px;margin-bottom:10px}
|
||||
.card-title{font-size:14px;font-weight:700;margin-bottom:4px}
|
||||
.card-desc{font-size:12px;color:var(--text2);line-height:1.5}
|
||||
.section{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;margin-bottom:14px}
|
||||
.section-h{font-size:15px;font-weight:700;margin-bottom:14px;display:flex;align-items:center;gap:8px}
|
||||
.step{display:flex;gap:12px;padding:12px 0;border-bottom:1px solid rgba(255,255,255,0.03)}
|
||||
.step:last-child{border:none}
|
||||
.step-num{width:28px;height:28px;border-radius:8px;background:rgba(14,165,233,0.1);color:var(--blue);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;flex-shrink:0}
|
||||
.step-info{flex:1}
|
||||
.step-title{font-size:13px;font-weight:600;margin-bottom:2px}
|
||||
.step-desc{font-size:11px;color:var(--text2)}
|
||||
.code{background:rgba(0,0,0,0.4);border-radius:8px;padding:12px 14px;font-family:'SF Mono',Monaco,monospace;font-size:12px;color:#38bdf8;margin:8px 0;overflow-x:auto}
|
||||
.nav-list a{display:flex;align-items:center;gap:10px;padding:10px 0;border-bottom:1px solid rgba(255,255,255,0.03);text-decoration:none;color:var(--text);transition:all .2s}
|
||||
.nav-list a:last-child{border:none}
|
||||
.nav-list a:active{opacity:.7}
|
||||
.nav-icon{width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:15px;flex-shrink:0}
|
||||
.nav-text{font-size:13px;font-weight:600}
|
||||
.nav-arrow{margin-left:auto;color:var(--text3);font-size:14px}
|
||||
.ft{text-align:center;padding:16px}
|
||||
.ft-b{display:inline-flex;align-items:center;gap:6px;font-size:10px;font-weight:600;color:var(--text3);padding:5px 12px;border-radius:20px;background:rgba(14,165,233,0.04);border:1px solid rgba(14,165,233,0.08)}
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}
|
||||
.card,.section{animation:fadeIn .5s ease backwards}
|
||||
.card:nth-child(1){animation-delay:.1s}.card:nth-child(2){animation-delay:.15s}.card:nth-child(3){animation-delay:.2s}.card:nth-child(4){animation-delay:.25s}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="c">
|
||||
<div class="hero">
|
||||
<div class="hero-icon">📖</div>
|
||||
<h1>Hexo 文档</h1>
|
||||
<p>快速、简洁且高效的博客框架,基于 Node.js 构建</p>
|
||||
<div class="badge-row">
|
||||
<span class="badge active">v7.0</span>
|
||||
<span class="badge">Node.js</span>
|
||||
<span class="badge">Markdown</span>
|
||||
<span class="badge">开源</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cards">
|
||||
<div class="card"><div class="card-icon">🚀</div><div class="card-title">快速入门</div><div class="card-desc">几分钟内创建你的第一个博客</div></div>
|
||||
<div class="card"><div class="card-icon">🔧</div><div class="card-title">配置</div><div class="card-desc">自定义网站的每个细节</div></div>
|
||||
<div class="card"><div class="card-icon">🎨</div><div class="card-title">主题</div><div class="card-desc">数百个精美主题可选</div></div>
|
||||
<div class="card"><div class="card-icon">🔌</div><div class="card-title">插件</div><div class="card-desc">丰富的插件生态系统</div></div>
|
||||
</div>
|
||||
<div class="section" style="animation-delay:.3s">
|
||||
<div class="section-h">⚡ 快速开始</div>
|
||||
<div class="step"><div class="step-num">1</div><div class="step-info"><div class="step-title">安装 Hexo</div><div class="code">npm install -g hexo-cli</div></div></div>
|
||||
<div class="step"><div class="step-num">2</div><div class="step-info"><div class="step-title">初始化项目</div><div class="code">hexo init my-blog && cd my-blog</div></div></div>
|
||||
<div class="step"><div class="step-num">3</div><div class="step-info"><div class="step-title">启动服务</div><div class="code">hexo server</div></div></div>
|
||||
</div>
|
||||
<div class="section" style="animation-delay:.35s">
|
||||
<div class="section-h">📚 文档导航</div>
|
||||
<div class="nav-list">
|
||||
<a href="#"><div class="nav-icon" style="background:rgba(14,165,233,0.1)">📝</div><span class="nav-text">写作</span><span class="nav-arrow">→</span></a>
|
||||
<a href="#"><div class="nav-icon" style="background:rgba(139,92,246,0.1)">🏷️</div><span class="nav-text">标签与分类</span><span class="nav-arrow">→</span></a>
|
||||
<a href="#"><div class="nav-icon" style="background:rgba(16,185,129,0.1)">🌐</div><span class="nav-text">部署</span><span class="nav-arrow">→</span></a>
|
||||
<a href="#"><div class="nav-icon" style="background:rgba(245,158,11,0.1)">⚙️</div><span class="nav-text">高级配置</span><span class="nav-arrow">→</span></a>
|
||||
<a href="#"><div class="nav-icon" style="background:rgba(236,72,153,0.1)">❓</div><span class="nav-text">常见问题</span><span class="nav-arrow">→</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ft"><div class="ft-b">📖 Hexo 文档 · 由 WebToApp 提供</div></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1
app/src/main/assets/sample_projects/docs-hexo/style.css
Normal file
1
app/src/main/assets/sample_projects/docs-hexo/style.css
Normal file
@ -0,0 +1 @@
|
||||
|
||||
309
app/src/main/assets/sample_projects/docs-mkdocs-ar/index.html
Normal file
309
app/src/main/assets/sample_projects/docs-mkdocs-ar/index.html
Normal file
@ -0,0 +1,309 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>وثائق MkDocs</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #0a0a1a;
|
||||
--card: rgba(255, 255, 255, 0.04);
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--text: #f0f0ff;
|
||||
--text2: rgba(240, 240, 255, 0.55);
|
||||
--text3: rgba(240, 240, 255, 0.3);
|
||||
--teal: #14b8a6;
|
||||
--radius: 14px
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Tahoma, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
direction: rtl
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: -40%;
|
||||
left: -40%;
|
||||
width: 180%;
|
||||
height: 180%;
|
||||
background: radial-gradient(ellipse at 50% 20%, rgba(20, 184, 166, 0.06) 0%, transparent 55%);
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
.c {
|
||||
max-width: 460px;
|
||||
margin: 0 auto;
|
||||
padding: 0 14px 32px;
|
||||
position: relative;
|
||||
z-index: 1
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 40px 0 32px
|
||||
}
|
||||
|
||||
.hero-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
animation: float 3s ease-in-out infinite
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0)
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-8px)
|
||||
}
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #14b8a6, #2dd4bf);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 8px
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 14px;
|
||||
color: var(--text2);
|
||||
line-height: 1.6;
|
||||
max-width: 320px;
|
||||
margin: 0 auto
|
||||
}
|
||||
|
||||
.badge-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text2)
|
||||
}
|
||||
|
||||
.badge.active {
|
||||
background: rgba(20, 184, 166, 0.1);
|
||||
color: var(--teal)
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
cursor: pointer;
|
||||
transition: all .2s
|
||||
}
|
||||
|
||||
.card:active {
|
||||
transform: scale(.97)
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text2);
|
||||
line-height: 1.5
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
margin-bottom: 14px
|
||||
}
|
||||
|
||||
.section-h {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 14px
|
||||
}
|
||||
|
||||
.code {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #2dd4bf;
|
||||
margin: 8px 0;
|
||||
direction: ltr;
|
||||
text-align: left
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03)
|
||||
}
|
||||
|
||||
.step:last-child {
|
||||
border: none
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: rgba(20, 184, 166, 0.1);
|
||||
color: var(--teal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.step-info {
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px
|
||||
}
|
||||
|
||||
.ft {
|
||||
text-align: center;
|
||||
padding: 16px
|
||||
}
|
||||
|
||||
.ft-b {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text3);
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
background: rgba(20, 184, 166, 0.04);
|
||||
border: 1px solid rgba(20, 184, 166, 0.08)
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px)
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0)
|
||||
}
|
||||
}
|
||||
|
||||
.card,
|
||||
.section {
|
||||
animation: fadeIn .5s ease backwards
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="c">
|
||||
<div class="hero">
|
||||
<div class="hero-icon">📗</div>
|
||||
<h1>وثائق MkDocs</h1>
|
||||
<p>أنشئ وثائق المشروع بـ Markdown مع سمة Material</p>
|
||||
<div class="badge-row"><span class="badge active">Material</span><span class="badge">Python</span><span
|
||||
class="badge">Markdown</span></div>
|
||||
</div>
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<div class="card-icon">🚀</div>
|
||||
<div class="card-title">بداية سريعة</div>
|
||||
<div class="card-desc">إعداد الوثائق في 5 دقائق</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🎨</div>
|
||||
<div class="card-title">سمة Material</div>
|
||||
<div class="card-desc">تصميم عصري ووضع داكن</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🔍</div>
|
||||
<div class="card-title">البحث</div>
|
||||
<div class="card-desc">محرك بحث نصي مدمج</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">📱</div>
|
||||
<div class="card-title">متجاوب</div>
|
||||
<div class="card-desc">دعم مثالي للهاتف</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" style="animation-delay:.3s">
|
||||
<div class="section-h">⚡ البداية السريعة</div>
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">تثبيت MkDocs</div>
|
||||
<div class="code">pip install mkdocs-material</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">إنشاء مشروع</div>
|
||||
<div class="code">mkdocs new my-docs && cd my-docs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">معاينة</div>
|
||||
<div class="code">mkdocs serve</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ft">
|
||||
<div class="ft-b">📗 MkDocs Material · مقدم من WebToApp</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -0,0 +1,2 @@
|
||||
site_name: MkDocs Demo
|
||||
theme: material
|
||||
14
app/src/main/assets/sample_projects/docs-mkdocs-ar/style.css
Normal file
14
app/src/main/assets/sample_projects/docs-mkdocs-ar/style.css
Normal file
@ -0,0 +1,14 @@
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;display:flex;min-height:100vh;background:#fafafa}
|
||||
.sidebar{width:240px;background:#fff;border-right:1px solid #eee;padding:20px}
|
||||
.logo{font-size:20px;font-weight:700;margin-bottom:24px;padding:8px 0}
|
||||
.sidebar a{display:block;padding:10px 16px;border-radius:8px;color:#333;text-decoration:none;margin-bottom:4px;font-size:14px}
|
||||
.sidebar a.active{background:#e8f5e9;color:#2e7d32;font-weight:600}
|
||||
.sidebar a:hover{background:#f5f5f5}
|
||||
.content{flex:1;padding:40px 60px;max-width:800px}
|
||||
h1{font-size:32px;color:#1a1a1a;margin-bottom:16px}
|
||||
p{color:#666;line-height:1.8;margin-bottom:24px}
|
||||
.features{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:32px}
|
||||
.feature{background:#fff;border:1px solid #eee;border-radius:12px;padding:20px}
|
||||
.feature h3{font-size:16px;margin-bottom:8px}
|
||||
.feature p{font-size:14px;color:#888;margin:0}
|
||||
325
app/src/main/assets/sample_projects/docs-mkdocs-en/index.html
Normal file
325
app/src/main/assets/sample_projects/docs-mkdocs-en/index.html
Normal file
@ -0,0 +1,325 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>MkDocs Docs</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #0a0a1a;
|
||||
--card: rgba(255, 255, 255, 0.04);
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--text: #f0f0ff;
|
||||
--text2: rgba(240, 240, 255, 0.55);
|
||||
--text3: rgba(240, 240, 255, 0.3);
|
||||
--teal: #14b8a6;
|
||||
--radius: 14px
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: -40%;
|
||||
left: -40%;
|
||||
width: 180%;
|
||||
height: 180%;
|
||||
background: radial-gradient(ellipse at 50% 20%, rgba(20, 184, 166, 0.06) 0%, transparent 55%);
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
.c {
|
||||
max-width: 460px;
|
||||
margin: 0 auto;
|
||||
padding: 0 14px 32px;
|
||||
position: relative;
|
||||
z-index: 1
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 40px 0 32px
|
||||
}
|
||||
|
||||
.hero-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
animation: float 3s ease-in-out infinite
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0)
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-8px)
|
||||
}
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #14b8a6, #2dd4bf);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 8px
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 14px;
|
||||
color: var(--text2);
|
||||
line-height: 1.6;
|
||||
max-width: 320px;
|
||||
margin: 0 auto
|
||||
}
|
||||
|
||||
.badge-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text2)
|
||||
}
|
||||
|
||||
.badge.active {
|
||||
background: rgba(20, 184, 166, 0.1);
|
||||
color: var(--teal)
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
cursor: pointer;
|
||||
transition: all .2s
|
||||
}
|
||||
|
||||
.card:active {
|
||||
transform: scale(.97)
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text2);
|
||||
line-height: 1.5
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
margin-bottom: 14px
|
||||
}
|
||||
|
||||
.section-h {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 14px
|
||||
}
|
||||
|
||||
.code {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #2dd4bf;
|
||||
margin: 8px 0
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03)
|
||||
}
|
||||
|
||||
.step:last-child {
|
||||
border: none
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: rgba(20, 184, 166, 0.1);
|
||||
color: var(--teal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.step-info {
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px
|
||||
}
|
||||
|
||||
.ft {
|
||||
text-align: center;
|
||||
padding: 16px
|
||||
}
|
||||
|
||||
.ft-b {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text3);
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
background: rgba(20, 184, 166, 0.04);
|
||||
border: 1px solid rgba(20, 184, 166, 0.08)
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px)
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0)
|
||||
}
|
||||
}
|
||||
|
||||
.card,
|
||||
.section {
|
||||
animation: fadeIn .5s ease backwards
|
||||
}
|
||||
|
||||
.card:nth-child(1) {
|
||||
animation-delay: .1s
|
||||
}
|
||||
|
||||
.card:nth-child(2) {
|
||||
animation-delay: .15s
|
||||
}
|
||||
|
||||
.card:nth-child(3) {
|
||||
animation-delay: .2s
|
||||
}
|
||||
|
||||
.card:nth-child(4) {
|
||||
animation-delay: .25s
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="c">
|
||||
<div class="hero">
|
||||
<div class="hero-icon">📗</div>
|
||||
<h1>MkDocs</h1>
|
||||
<p>Build project documentation with Markdown, powered by Material theme</p>
|
||||
<div class="badge-row"><span class="badge active">Material</span><span class="badge">Python</span><span
|
||||
class="badge">Markdown</span></div>
|
||||
</div>
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<div class="card-icon">🚀</div>
|
||||
<div class="card-title">Quick Start</div>
|
||||
<div class="card-desc">Set up docs in 5 minutes</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🎨</div>
|
||||
<div class="card-title">Material Theme</div>
|
||||
<div class="card-desc">Modern design, dark mode</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🔍</div>
|
||||
<div class="card-title">Search</div>
|
||||
<div class="card-desc">Built-in full-text search</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">📱</div>
|
||||
<div class="card-title">Responsive</div>
|
||||
<div class="card-desc">Perfect mobile support</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" style="animation-delay:.3s">
|
||||
<div class="section-h">⚡ Getting Started</div>
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">Install MkDocs</div>
|
||||
<div class="code">pip install mkdocs-material</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">Create Project</div>
|
||||
<div class="code">mkdocs new my-docs && cd my-docs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">Start Preview</div>
|
||||
<div class="code">mkdocs serve</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ft">
|
||||
<div class="ft-b">📗 MkDocs Material · Powered by WebToApp</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -0,0 +1,2 @@
|
||||
site_name: MkDocs Demo
|
||||
theme: material
|
||||
14
app/src/main/assets/sample_projects/docs-mkdocs-en/style.css
Normal file
14
app/src/main/assets/sample_projects/docs-mkdocs-en/style.css
Normal file
@ -0,0 +1,14 @@
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;display:flex;min-height:100vh;background:#fafafa}
|
||||
.sidebar{width:240px;background:#fff;border-right:1px solid #eee;padding:20px}
|
||||
.logo{font-size:20px;font-weight:700;margin-bottom:24px;padding:8px 0}
|
||||
.sidebar a{display:block;padding:10px 16px;border-radius:8px;color:#333;text-decoration:none;margin-bottom:4px;font-size:14px}
|
||||
.sidebar a.active{background:#e8f5e9;color:#2e7d32;font-weight:600}
|
||||
.sidebar a:hover{background:#f5f5f5}
|
||||
.content{flex:1;padding:40px 60px;max-width:800px}
|
||||
h1{font-size:32px;color:#1a1a1a;margin-bottom:16px}
|
||||
p{color:#666;line-height:1.8;margin-bottom:24px}
|
||||
.features{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:32px}
|
||||
.feature{background:#fff;border:1px solid #eee;border-radius:12px;padding:20px}
|
||||
.feature h3{font-size:16px;margin-bottom:8px}
|
||||
.feature p{font-size:14px;color:#888;margin:0}
|
||||
388
app/src/main/assets/sample_projects/docs-mkdocs/index.html
Normal file
388
app/src/main/assets/sample_projects/docs-mkdocs/index.html
Normal file
@ -0,0 +1,388 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>MkDocs 文档</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #0a0a1a;
|
||||
--card: rgba(255, 255, 255, 0.04);
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--text: #f0f0ff;
|
||||
--text2: rgba(240, 240, 255, 0.55);
|
||||
--text3: rgba(240, 240, 255, 0.3);
|
||||
--teal: #14b8a6;
|
||||
--radius: 14px
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
-webkit-font-smoothing: antialiased
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: -40%;
|
||||
left: -40%;
|
||||
width: 180%;
|
||||
height: 180%;
|
||||
background: radial-gradient(ellipse at 50% 20%, rgba(20, 184, 166, 0.06) 0%, transparent 55%);
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
.c {
|
||||
max-width: 460px;
|
||||
margin: 0 auto;
|
||||
padding: 0 14px 32px;
|
||||
position: relative;
|
||||
z-index: 1
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 40px 0 32px
|
||||
}
|
||||
|
||||
.hero-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
animation: float 3s ease-in-out infinite
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0)
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-8px)
|
||||
}
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #14b8a6, #2dd4bf);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 8px
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 14px;
|
||||
color: var(--text2);
|
||||
line-height: 1.6;
|
||||
max-width: 320px;
|
||||
margin: 0 auto
|
||||
}
|
||||
|
||||
.badge-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text2)
|
||||
}
|
||||
|
||||
.badge.active {
|
||||
background: rgba(20, 184, 166, 0.1);
|
||||
color: var(--teal);
|
||||
border-color: rgba(20, 184, 166, 0.2)
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
backdrop-filter: blur(16px);
|
||||
cursor: pointer;
|
||||
transition: all .2s
|
||||
}
|
||||
|
||||
.card:active {
|
||||
transform: scale(.97)
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text2);
|
||||
line-height: 1.5
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
margin-bottom: 14px
|
||||
}
|
||||
|
||||
.section-h {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px
|
||||
}
|
||||
|
||||
.code {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 12px;
|
||||
color: #2dd4bf;
|
||||
margin: 8px 0;
|
||||
overflow-x: auto
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03)
|
||||
}
|
||||
|
||||
.step:last-child {
|
||||
border: none
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: rgba(20, 184, 166, 0.1);
|
||||
color: var(--teal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.step-info {
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px
|
||||
}
|
||||
|
||||
.nav-list a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
|
||||
text-decoration: none;
|
||||
color: var(--text)
|
||||
}
|
||||
|
||||
.nav-list a:last-child {
|
||||
border: none
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 13px;
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
.nav-arrow {
|
||||
margin-left: auto;
|
||||
color: var(--text3)
|
||||
}
|
||||
|
||||
.ft {
|
||||
text-align: center;
|
||||
padding: 16px
|
||||
}
|
||||
|
||||
.ft-b {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text3);
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
background: rgba(20, 184, 166, 0.04);
|
||||
border: 1px solid rgba(20, 184, 166, 0.08)
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px)
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0)
|
||||
}
|
||||
}
|
||||
|
||||
.card,
|
||||
.section {
|
||||
animation: fadeIn .5s ease backwards
|
||||
}
|
||||
|
||||
.card:nth-child(1) {
|
||||
animation-delay: .1s
|
||||
}
|
||||
|
||||
.card:nth-child(2) {
|
||||
animation-delay: .15s
|
||||
}
|
||||
|
||||
.card:nth-child(3) {
|
||||
animation-delay: .2s
|
||||
}
|
||||
|
||||
.card:nth-child(4) {
|
||||
animation-delay: .25s
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="c">
|
||||
<div class="hero">
|
||||
<div class="hero-icon">📗</div>
|
||||
<h1>MkDocs 文档</h1>
|
||||
<p>使用 Markdown 构建项目文档,Material 主题打造极致体验</p>
|
||||
<div class="badge-row"><span class="badge active">Material</span><span class="badge">Python</span><span
|
||||
class="badge">Markdown</span><span class="badge">开源</span></div>
|
||||
</div>
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<div class="card-icon">🚀</div>
|
||||
<div class="card-title">快速入门</div>
|
||||
<div class="card-desc">5 分钟搭建文档站</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🎨</div>
|
||||
<div class="card-title">Material 主题</div>
|
||||
<div class="card-desc">现代化设计,深色模式</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🔍</div>
|
||||
<div class="card-title">搜索</div>
|
||||
<div class="card-desc">内置全文搜索引擎</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">📱</div>
|
||||
<div class="card-title">响应式</div>
|
||||
<div class="card-desc">完美适配移动设备</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" style="animation-delay:.3s">
|
||||
<div class="section-h">⚡ 快速开始</div>
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">安装 MkDocs</div>
|
||||
<div class="code">pip install mkdocs-material</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">新建项目</div>
|
||||
<div class="code">mkdocs new my-docs && cd my-docs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">启动预览</div>
|
||||
<div class="code">mkdocs serve</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" style="animation-delay:.35s">
|
||||
<div class="section-h">📚 文档导航</div>
|
||||
<div class="nav-list">
|
||||
<a href="#">
|
||||
<div class="nav-icon" style="background:rgba(20,184,166,0.1)">📐</div><span
|
||||
class="nav-text">配置</span><span class="nav-arrow">→</span>
|
||||
</a>
|
||||
<a href="#">
|
||||
<div class="nav-icon" style="background:rgba(139,92,246,0.1)">🎯</div><span
|
||||
class="nav-text">自定义主题</span><span class="nav-arrow">→</span>
|
||||
</a>
|
||||
<a href="#">
|
||||
<div class="nav-icon" style="background:rgba(16,185,129,0.1)">📦</div><span
|
||||
class="nav-text">插件扩展</span><span class="nav-arrow">→</span>
|
||||
</a>
|
||||
<a href="#">
|
||||
<div class="nav-icon" style="background:rgba(245,158,11,0.1)">🌐</div><span
|
||||
class="nav-text">部署</span><span class="nav-arrow">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ft">
|
||||
<div class="ft-b">📗 MkDocs Material · 由 WebToApp 提供</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -0,0 +1,2 @@
|
||||
site_name: MkDocs Demo
|
||||
theme: material
|
||||
14
app/src/main/assets/sample_projects/docs-mkdocs/style.css
Normal file
14
app/src/main/assets/sample_projects/docs-mkdocs/style.css
Normal file
@ -0,0 +1,14 @@
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;display:flex;min-height:100vh;background:#fafafa}
|
||||
.sidebar{width:240px;background:#fff;border-right:1px solid #eee;padding:20px}
|
||||
.logo{font-size:20px;font-weight:700;margin-bottom:24px;padding:8px 0}
|
||||
.sidebar a{display:block;padding:10px 16px;border-radius:8px;color:#333;text-decoration:none;margin-bottom:4px;font-size:14px}
|
||||
.sidebar a.active{background:#e8f5e9;color:#2e7d32;font-weight:600}
|
||||
.sidebar a:hover{background:#f5f5f5}
|
||||
.content{flex:1;padding:40px 60px;max-width:800px}
|
||||
h1{font-size:32px;color:#1a1a1a;margin-bottom:16px}
|
||||
p{color:#666;line-height:1.8;margin-bottom:24px}
|
||||
.features{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:32px}
|
||||
.feature{background:#fff;border:1px solid #eee;border-radius:12px;padding:20px}
|
||||
.feature h3{font-size:16px;margin-bottom:8px}
|
||||
.feature p{font-size:14px;color:#888;margin:0}
|
||||
@ -0,0 +1 @@
|
||||
{}
|
||||
309
app/src/main/assets/sample_projects/docs-vitepress-ar/index.html
Normal file
309
app/src/main/assets/sample_projects/docs-vitepress-ar/index.html
Normal file
@ -0,0 +1,309 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>وثائق VitePress</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #0a0a1a;
|
||||
--card: rgba(255, 255, 255, 0.04);
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--text: #f0f0ff;
|
||||
--text2: rgba(240, 240, 255, 0.55);
|
||||
--text3: rgba(240, 240, 255, 0.3);
|
||||
--green: #10b981;
|
||||
--radius: 14px
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Tahoma, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
direction: rtl
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: -40%;
|
||||
left: -40%;
|
||||
width: 180%;
|
||||
height: 180%;
|
||||
background: radial-gradient(ellipse at 50% 20%, rgba(16, 185, 129, 0.06) 0%, transparent 55%);
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
.c {
|
||||
max-width: 460px;
|
||||
margin: 0 auto;
|
||||
padding: 0 14px 32px;
|
||||
position: relative;
|
||||
z-index: 1
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 40px 0 32px
|
||||
}
|
||||
|
||||
.hero-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
animation: float 3s ease-in-out infinite
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0)
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-8px)
|
||||
}
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #10b981, #34d399);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 8px
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 14px;
|
||||
color: var(--text2);
|
||||
line-height: 1.6;
|
||||
max-width: 320px;
|
||||
margin: 0 auto
|
||||
}
|
||||
|
||||
.badge-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text2)
|
||||
}
|
||||
|
||||
.badge.active {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--green)
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
cursor: pointer;
|
||||
transition: all .2s
|
||||
}
|
||||
|
||||
.card:active {
|
||||
transform: scale(.97)
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text2);
|
||||
line-height: 1.5
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
margin-bottom: 14px
|
||||
}
|
||||
|
||||
.section-h {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 14px
|
||||
}
|
||||
|
||||
.code {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #34d399;
|
||||
margin: 8px 0;
|
||||
direction: ltr;
|
||||
text-align: left
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03)
|
||||
}
|
||||
|
||||
.step:last-child {
|
||||
border: none
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--green);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.step-info {
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px
|
||||
}
|
||||
|
||||
.ft {
|
||||
text-align: center;
|
||||
padding: 16px
|
||||
}
|
||||
|
||||
.ft-b {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text3);
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
background: rgba(16, 185, 129, 0.04);
|
||||
border: 1px solid rgba(16, 185, 129, 0.08)
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px)
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0)
|
||||
}
|
||||
}
|
||||
|
||||
.card,
|
||||
.section {
|
||||
animation: fadeIn .5s ease backwards
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="c">
|
||||
<div class="hero">
|
||||
<div class="hero-icon">⚡</div>
|
||||
<h1>VitePress</h1>
|
||||
<p>مولد مواقع ثابتة مدعوم بـ Vite مصمم للتوثيق</p>
|
||||
<div class="badge-row"><span class="badge active">v1.0</span><span class="badge">Vue 3</span><span
|
||||
class="badge">Vite</span></div>
|
||||
</div>
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<div class="card-icon">⚡</div>
|
||||
<div class="card-title">فائق السرعة</div>
|
||||
<div class="card-desc">إعادة تحميل فورية</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">📝</div>
|
||||
<div class="card-title">Markdown</div>
|
||||
<div class="card-desc">صيغة Markdown محسنة</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🎨</div>
|
||||
<div class="card-title">السمات</div>
|
||||
<div class="card-desc">سمة افتراضية جميلة</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🔧</div>
|
||||
<div class="card-title">قابل للتخصيص</div>
|
||||
<div class="card-desc">تكامل عميق مع Vue</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" style="animation-delay:.3s">
|
||||
<div class="section-h">🚀 البداية السريعة</div>
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">إنشاء</div>
|
||||
<div class="code">npx vitepress init</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">تطوير</div>
|
||||
<div class="code">npx vitepress dev docs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">بناء</div>
|
||||
<div class="code">npx vitepress build docs</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ft">
|
||||
<div class="ft-b">⚡ VitePress · مقدم من WebToApp</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -0,0 +1,14 @@
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;display:flex;min-height:100vh;background:#fafafa}
|
||||
.sidebar{width:240px;background:#fff;border-right:1px solid #eee;padding:20px}
|
||||
.logo{font-size:20px;font-weight:700;margin-bottom:24px;padding:8px 0}
|
||||
.sidebar a{display:block;padding:10px 16px;border-radius:8px;color:#333;text-decoration:none;margin-bottom:4px;font-size:14px}
|
||||
.sidebar a.active{background:#e8f5e9;color:#2e7d32;font-weight:600}
|
||||
.sidebar a:hover{background:#f5f5f5}
|
||||
.content{flex:1;padding:40px 60px;max-width:800px}
|
||||
h1{font-size:32px;color:#1a1a1a;margin-bottom:16px}
|
||||
p{color:#666;line-height:1.8;margin-bottom:24px}
|
||||
.features{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:32px}
|
||||
.feature{background:#fff;border:1px solid #eee;border-radius:12px;padding:20px}
|
||||
.feature h3{font-size:16px;margin-bottom:8px}
|
||||
.feature p{font-size:14px;color:#888;margin:0}
|
||||
@ -0,0 +1 @@
|
||||
{}
|
||||
383
app/src/main/assets/sample_projects/docs-vitepress-en/index.html
Normal file
383
app/src/main/assets/sample_projects/docs-vitepress-en/index.html
Normal file
@ -0,0 +1,383 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>VitePress Docs</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #0a0a1a;
|
||||
--card: rgba(255, 255, 255, 0.04);
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--text: #f0f0ff;
|
||||
--text2: rgba(240, 240, 255, 0.55);
|
||||
--text3: rgba(240, 240, 255, 0.3);
|
||||
--green: #10b981;
|
||||
--radius: 14px
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: -40%;
|
||||
left: -40%;
|
||||
width: 180%;
|
||||
height: 180%;
|
||||
background: radial-gradient(ellipse at 50% 20%, rgba(16, 185, 129, 0.06) 0%, transparent 55%);
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
.c {
|
||||
max-width: 460px;
|
||||
margin: 0 auto;
|
||||
padding: 0 14px 32px;
|
||||
position: relative;
|
||||
z-index: 1
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 40px 0 32px
|
||||
}
|
||||
|
||||
.hero-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
animation: float 3s ease-in-out infinite
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0)
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-8px)
|
||||
}
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #10b981, #34d399);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 8px
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 14px;
|
||||
color: var(--text2);
|
||||
line-height: 1.6;
|
||||
max-width: 320px;
|
||||
margin: 0 auto
|
||||
}
|
||||
|
||||
.badge-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text2)
|
||||
}
|
||||
|
||||
.badge.active {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--green)
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
cursor: pointer;
|
||||
transition: all .2s
|
||||
}
|
||||
|
||||
.card:active {
|
||||
transform: scale(.97)
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text2);
|
||||
line-height: 1.5
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
margin-bottom: 14px
|
||||
}
|
||||
|
||||
.section-h {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 14px
|
||||
}
|
||||
|
||||
.code {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #34d399;
|
||||
margin: 8px 0
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03)
|
||||
}
|
||||
|
||||
.step:last-child {
|
||||
border: none
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--green);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.step-info {
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px
|
||||
}
|
||||
|
||||
.feature {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03)
|
||||
}
|
||||
|
||||
.feature:last-child {
|
||||
border: none
|
||||
}
|
||||
|
||||
.feat-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.feat-info {
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.feat-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
.feat-desc {
|
||||
font-size: 11px;
|
||||
color: var(--text2)
|
||||
}
|
||||
|
||||
.ft {
|
||||
text-align: center;
|
||||
padding: 16px
|
||||
}
|
||||
|
||||
.ft-b {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text3);
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
background: rgba(16, 185, 129, 0.04);
|
||||
border: 1px solid rgba(16, 185, 129, 0.08)
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px)
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0)
|
||||
}
|
||||
}
|
||||
|
||||
.card,
|
||||
.section {
|
||||
animation: fadeIn .5s ease backwards
|
||||
}
|
||||
|
||||
.card:nth-child(1) {
|
||||
animation-delay: .1s
|
||||
}
|
||||
|
||||
.card:nth-child(2) {
|
||||
animation-delay: .15s
|
||||
}
|
||||
|
||||
.card:nth-child(3) {
|
||||
animation-delay: .2s
|
||||
}
|
||||
|
||||
.card:nth-child(4) {
|
||||
animation-delay: .25s
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="c">
|
||||
<div class="hero">
|
||||
<div class="hero-icon">⚡</div>
|
||||
<h1>VitePress</h1>
|
||||
<p>Vite-powered static site generator built for documentation</p>
|
||||
<div class="badge-row"><span class="badge active">v1.0</span><span class="badge">Vue 3</span><span
|
||||
class="badge">Vite</span><span class="badge">SSG</span></div>
|
||||
</div>
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<div class="card-icon">⚡</div>
|
||||
<div class="card-title">Lightning Fast</div>
|
||||
<div class="card-desc">Instant hot reload</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">📝</div>
|
||||
<div class="card-title">Markdown</div>
|
||||
<div class="card-desc">Enhanced Markdown syntax</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🎨</div>
|
||||
<div class="card-title">Themes</div>
|
||||
<div class="card-desc">Beautiful default theme</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🔧</div>
|
||||
<div class="card-title">Customizable</div>
|
||||
<div class="card-desc">Deep Vue integration</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" style="animation-delay:.3s">
|
||||
<div class="section-h">🚀 Getting Started</div>
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">Initialize</div>
|
||||
<div class="code">npx vitepress init</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">Develop</div>
|
||||
<div class="code">npx vitepress dev docs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">Build</div>
|
||||
<div class="code">npx vitepress build docs</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" style="animation-delay:.35s">
|
||||
<div class="section-h">✨ Features</div>
|
||||
<div class="feature">
|
||||
<div class="feat-icon" style="background:rgba(16,185,129,0.1)">⚡</div>
|
||||
<div class="feat-info">
|
||||
<div class="feat-title">Vite Powered</div>
|
||||
<div class="feat-desc">Instant server start + blazing fast HMR</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feat-icon" style="background:rgba(139,92,246,0.1)">🎯</div>
|
||||
<div class="feat-info">
|
||||
<div class="feat-title">Vue 3 Integration</div>
|
||||
<div class="feat-desc">Embed Vue components in Markdown</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feat-icon" style="background:rgba(14,165,233,0.1)">🔍</div>
|
||||
<div class="feat-info">
|
||||
<div class="feat-title">Built-in Search</div>
|
||||
<div class="feat-desc">Full-text search powered by minisearch</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ft">
|
||||
<div class="ft-b">⚡ VitePress · Powered by WebToApp</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -0,0 +1,14 @@
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;display:flex;min-height:100vh;background:#fafafa}
|
||||
.sidebar{width:240px;background:#fff;border-right:1px solid #eee;padding:20px}
|
||||
.logo{font-size:20px;font-weight:700;margin-bottom:24px;padding:8px 0}
|
||||
.sidebar a{display:block;padding:10px 16px;border-radius:8px;color:#333;text-decoration:none;margin-bottom:4px;font-size:14px}
|
||||
.sidebar a.active{background:#e8f5e9;color:#2e7d32;font-weight:600}
|
||||
.sidebar a:hover{background:#f5f5f5}
|
||||
.content{flex:1;padding:40px 60px;max-width:800px}
|
||||
h1{font-size:32px;color:#1a1a1a;margin-bottom:16px}
|
||||
p{color:#666;line-height:1.8;margin-bottom:24px}
|
||||
.features{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:32px}
|
||||
.feature{background:#fff;border:1px solid #eee;border-radius:12px;padding:20px}
|
||||
.feature h3{font-size:16px;margin-bottom:8px}
|
||||
.feature p{font-size:14px;color:#888;margin:0}
|
||||
@ -0,0 +1 @@
|
||||
{}
|
||||
398
app/src/main/assets/sample_projects/docs-vitepress/index.html
Normal file
398
app/src/main/assets/sample_projects/docs-vitepress/index.html
Normal file
@ -0,0 +1,398 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>VitePress 文档</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #0a0a1a;
|
||||
--card: rgba(255, 255, 255, 0.04);
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--text: #f0f0ff;
|
||||
--text2: rgba(240, 240, 255, 0.55);
|
||||
--text3: rgba(240, 240, 255, 0.3);
|
||||
--green: #10b981;
|
||||
--radius: 14px
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
-webkit-font-smoothing: antialiased
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: -40%;
|
||||
left: -40%;
|
||||
width: 180%;
|
||||
height: 180%;
|
||||
background: radial-gradient(ellipse at 50% 20%, rgba(16, 185, 129, 0.06) 0%, transparent 55%);
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
.c {
|
||||
max-width: 460px;
|
||||
margin: 0 auto;
|
||||
padding: 0 14px 32px;
|
||||
position: relative;
|
||||
z-index: 1
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 40px 0 32px
|
||||
}
|
||||
|
||||
.hero-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
animation: float 3s ease-in-out infinite
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0)
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-8px)
|
||||
}
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #10b981, #34d399);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 8px
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 14px;
|
||||
color: var(--text2);
|
||||
line-height: 1.6;
|
||||
max-width: 320px;
|
||||
margin: 0 auto
|
||||
}
|
||||
|
||||
.badge-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text2)
|
||||
}
|
||||
|
||||
.badge.active {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--green);
|
||||
border-color: rgba(16, 185, 129, 0.2)
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
cursor: pointer;
|
||||
transition: all .2s
|
||||
}
|
||||
|
||||
.card:active {
|
||||
transform: scale(.97)
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text2);
|
||||
line-height: 1.5
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
margin-bottom: 14px
|
||||
}
|
||||
|
||||
.section-h {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px
|
||||
}
|
||||
|
||||
.code {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 12px;
|
||||
color: #34d399;
|
||||
margin: 8px 0
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03)
|
||||
}
|
||||
|
||||
.step:last-child {
|
||||
border: none
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--green);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.step-info {
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px
|
||||
}
|
||||
|
||||
.feature {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03)
|
||||
}
|
||||
|
||||
.feature:last-child {
|
||||
border: none
|
||||
}
|
||||
|
||||
.feat-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.feat-info {
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.feat-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
.feat-desc {
|
||||
font-size: 11px;
|
||||
color: var(--text2)
|
||||
}
|
||||
|
||||
.ft {
|
||||
text-align: center;
|
||||
padding: 16px
|
||||
}
|
||||
|
||||
.ft-b {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text3);
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
background: rgba(16, 185, 129, 0.04);
|
||||
border: 1px solid rgba(16, 185, 129, 0.08)
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px)
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0)
|
||||
}
|
||||
}
|
||||
|
||||
.card,
|
||||
.section {
|
||||
animation: fadeIn .5s ease backwards
|
||||
}
|
||||
|
||||
.card:nth-child(1) {
|
||||
animation-delay: .1s
|
||||
}
|
||||
|
||||
.card:nth-child(2) {
|
||||
animation-delay: .15s
|
||||
}
|
||||
|
||||
.card:nth-child(3) {
|
||||
animation-delay: .2s
|
||||
}
|
||||
|
||||
.card:nth-child(4) {
|
||||
animation-delay: .25s
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="c">
|
||||
<div class="hero">
|
||||
<div class="hero-icon">⚡</div>
|
||||
<h1>VitePress</h1>
|
||||
<p>由 Vite 驱动的静态站点生成器,为文档而生</p>
|
||||
<div class="badge-row"><span class="badge active">v1.0</span><span class="badge">Vue 3</span><span
|
||||
class="badge">Vite</span><span class="badge">SSG</span></div>
|
||||
</div>
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<div class="card-icon">⚡</div>
|
||||
<div class="card-title">极速启动</div>
|
||||
<div class="card-desc">毫秒级热更新体验</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">📝</div>
|
||||
<div class="card-title">Markdown</div>
|
||||
<div class="card-desc">增强版 Markdown 语法</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🎨</div>
|
||||
<div class="card-title">主题</div>
|
||||
<div class="card-desc">默认主题即开即用</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🔧</div>
|
||||
<div class="card-title">自定义</div>
|
||||
<div class="card-desc">Vue 组件深度集成</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" style="animation-delay:.3s">
|
||||
<div class="section-h">🚀 快速开始</div>
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">安装 VitePress</div>
|
||||
<div class="code">npx vitepress init</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">启动开发</div>
|
||||
<div class="code">npx vitepress dev docs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">构建发布</div>
|
||||
<div class="code">npx vitepress build docs</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" style="animation-delay:.35s">
|
||||
<div class="section-h">✨ 核心特性</div>
|
||||
<div class="feature">
|
||||
<div class="feat-icon" style="background:rgba(16,185,129,0.1)">⚡</div>
|
||||
<div class="feat-info">
|
||||
<div class="feat-title">Vite 驱动</div>
|
||||
<div class="feat-desc">即时服务器启动 + 闪电般的 HMR</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feat-icon" style="background:rgba(139,92,246,0.1)">🎯</div>
|
||||
<div class="feat-info">
|
||||
<div class="feat-title">Vue 3 集成</div>
|
||||
<div class="feat-desc">在 Markdown 中嵌入 Vue 组件</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feat-icon" style="background:rgba(14,165,233,0.1)">🔍</div>
|
||||
<div class="feat-info">
|
||||
<div class="feat-title">内置搜索</div>
|
||||
<div class="feat-desc">基于 minisearch 的全文搜索</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feat-icon" style="background:rgba(245,158,11,0.1)">📊</div>
|
||||
<div class="feat-info">
|
||||
<div class="feat-title">SEO 优化</div>
|
||||
<div class="feat-desc">自动生成 sitemap + meta 标签</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ft">
|
||||
<div class="ft-b">⚡ VitePress · 由 WebToApp 提供</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
14
app/src/main/assets/sample_projects/docs-vitepress/style.css
Normal file
14
app/src/main/assets/sample_projects/docs-vitepress/style.css
Normal file
@ -0,0 +1,14 @@
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;display:flex;min-height:100vh;background:#fafafa}
|
||||
.sidebar{width:240px;background:#fff;border-right:1px solid #eee;padding:20px}
|
||||
.logo{font-size:20px;font-weight:700;margin-bottom:24px;padding:8px 0}
|
||||
.sidebar a{display:block;padding:10px 16px;border-radius:8px;color:#333;text-decoration:none;margin-bottom:4px;font-size:14px}
|
||||
.sidebar a.active{background:#e8f5e9;color:#2e7d32;font-weight:600}
|
||||
.sidebar a:hover{background:#f5f5f5}
|
||||
.content{flex:1;padding:40px 60px;max-width:800px}
|
||||
h1{font-size:32px;color:#1a1a1a;margin-bottom:16px}
|
||||
p{color:#666;line-height:1.8;margin-bottom:24px}
|
||||
.features{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:32px}
|
||||
.feature{background:#fff;border:1px solid #eee;border-radius:12px;padding:20px}
|
||||
.feature h3{font-size:16px;margin-bottom:8px}
|
||||
.feature p{font-size:14px;color:#888;margin:0}
|
||||
5
app/src/main/assets/sample_projects/go-echo-ar/go.mod
Normal file
5
app/src/main/assets/sample_projects/go-echo-ar/go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module github.com/demo/echo-server
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/labstack/echo/v4 v4.11.4
|
||||
25
app/src/main/assets/sample_projects/go-echo-ar/main.go
Normal file
25
app/src/main/assets/sample_projects/go-echo-ar/main.go
Normal file
@ -0,0 +1,25 @@
|
||||
package main
|
||||
// Echo Service Monitor (AR)
|
||||
import ("fmt";"math/rand";"net/http";"os";"time";"github.com/labstack/echo/v4";"github.com/labstack/echo/v4/middleware")
|
||||
var startTime = time.Now()
|
||||
func main() {
|
||||
e := echo.New(); e.HideBanner = true; e.Use(middleware.Logger())
|
||||
e.GET("/", func(c echo.Context) error { return c.HTML(http.StatusOK, dash()) })
|
||||
e.GET("/health", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]interface{}{"status":"healthy"}) })
|
||||
port := os.Getenv("PORT"); if port == "" { port = "1323" }; e.Logger.Fatal(e.Start(":"+port))
|
||||
}
|
||||
func dash() string {
|
||||
up := time.Since(startTime).Round(time.Second)
|
||||
svcs := []struct{n,s,i,c string}{{"API Gateway","يعمل","🌐","#00d4aa"},{"قاعدة البيانات","يعمل","🗄️","#3b82f6"},{"التخزين المؤقت","يعمل","⚡","#f59e0b"},{"قائمة الرسائل","يعمل","📨","#8b5cf6"},{"خدمة المصادقة","يعمل","🔐","#ec4899"},{"التخزين","يعمل","💾","#06b6d4"}}
|
||||
sh := ""; for _,s := range svcs { sh += fmt.Sprintf(`<div style="display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid rgba(255,255,255,0.03)"><div style="width:36px;height:36px;border-radius:10px;background:%s18;display:flex;align-items:center;justify-content:center;font-size:16px">%s</div><div style="flex:1"><div style="font-size:13px;font-weight:600">%s</div><div style="font-size:11px;color:%s">● %s</div></div></div>`,s.c,s.i,s.n,s.c,s.s) }
|
||||
bars := ""; days := []string{"إثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت","أحد"}
|
||||
for i,d := range days { h:=30+rand.Intn(60); bars+=fmt.Sprintf(`<div style="flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;height:100%%;justify-content:flex-end"><div style="width:100%%;max-width:28px;height:%d%%;border-radius:5px 5px 3px 3px;background:linear-gradient(180deg,#22d3ee,#0891b2);transform-origin:bottom;animation:grow .7s cubic-bezier(.34,1.56,.64,1) backwards;animation-delay:%.2fs"></div><div style="font-size:9px;color:rgba(240,240,255,0.3)">%s</div></div>`,h,0.3+float64(i)*0.08,d) }
|
||||
return fmt.Sprintf(`<!DOCTYPE html><html lang="ar" dir="rtl"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>Echo</title><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Tahoma,sans-serif;background:#080816;color:#f0f0ff;min-height:100vh;direction:rtl}@keyframes grow{from{transform:scaleY(0)}to{transform:scaleY(1)}}</style></head><body>
|
||||
<div style="max-width:460px;margin:0 auto;padding:0 14px 32px">
|
||||
<div style="padding:20px 2px;display:flex;justify-content:space-between;align-items:center"><div><h1 style="font-size:22px;font-weight:800;background:linear-gradient(135deg,#06b6d4,#22d3ee);-webkit-background-clip:text;-webkit-text-fill-color:transparent">📡 Echo مراقب</h1><p style="font-size:12px;color:rgba(240,240,255,0.55)">لوحة مراقبة الخدمات</p></div><div style="font-size:11px;font-weight:600;padding:5px 12px;border-radius:20px;background:rgba(6,182,212,0.1);color:#06b6d4">الكل يعمل</div></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px"><div style="background:rgba(255,255,255,0.035);border:1px solid rgba(255,255,255,0.07);border-radius:14px;padding:14px;text-align:center"><div style="font-size:20px;font-weight:800;color:#00d4aa">%s</div><div style="font-size:10px;color:rgba(240,240,255,0.55)">وقت التشغيل</div></div><div style="background:rgba(255,255,255,0.035);border:1px solid rgba(255,255,255,0.07);border-radius:14px;padding:14px;text-align:center"><div style="font-size:20px;font-weight:800;color:#06b6d4">6</div><div style="font-size:10px;color:rgba(240,240,255,0.55)">الخدمات</div></div><div style="background:rgba(255,255,255,0.035);border:1px solid rgba(255,255,255,0.07);border-radius:14px;padding:14px;text-align:center"><div style="font-size:20px;font-weight:800;color:#8b5cf6">99.9%%</div><div style="font-size:10px;color:rgba(240,240,255,0.55)">التوفر</div></div></div>
|
||||
<div style="background:rgba(255,255,255,0.035);border:1px solid rgba(255,255,255,0.07);border-radius:14px;padding:18px;margin-bottom:14px"><div style="font-size:15px;font-weight:700;margin-bottom:14px">حالة الخدمات</div>%s</div>
|
||||
<div style="background:rgba(255,255,255,0.035);border:1px solid rgba(255,255,255,0.07);border-radius:14px;padding:18px;margin-bottom:14px"><div style="font-size:15px;font-weight:700;margin-bottom:14px">حركة الزيارات</div><div style="display:flex;align-items:flex-end;justify-content:space-between;height:120px;gap:6px;direction:ltr">%s</div></div>
|
||||
<div style="text-align:center;padding:12px;font-size:10px;color:rgba(240,240,255,0.3)">Echo v4 · Go 1.22 · يعمل</div>
|
||||
</div></body></html>`,up,sh,bars)
|
||||
}
|
||||
5
app/src/main/assets/sample_projects/go-echo-en/go.mod
Normal file
5
app/src/main/assets/sample_projects/go-echo-en/go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module github.com/demo/echo-server
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/labstack/echo/v4 v4.11.4
|
||||
24
app/src/main/assets/sample_projects/go-echo-en/main.go
Normal file
24
app/src/main/assets/sample_projects/go-echo-en/main.go
Normal file
@ -0,0 +1,24 @@
|
||||
package main
|
||||
// Echo Service Monitor (EN)
|
||||
import ("fmt";"math/rand";"net/http";"os";"time";"github.com/labstack/echo/v4";"github.com/labstack/echo/v4/middleware")
|
||||
var startTime = time.Now()
|
||||
func main() {
|
||||
e := echo.New(); e.HideBanner = true; e.Use(middleware.Logger())
|
||||
e.GET("/", func(c echo.Context) error { return c.HTML(http.StatusOK, dash()) })
|
||||
e.GET("/health", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]interface{}{"status":"healthy","uptime":time.Since(startTime).String()}) })
|
||||
e.GET("/api/services", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]interface{}{"services":[]map[string]string{{"name":"API Gateway","status":"running"},{"name":"Database","status":"running"},{"name":"Cache","status":"running"}}}) })
|
||||
port := os.Getenv("PORT"); if port == "" { port = "1323" }; e.Logger.Fatal(e.Start(":"+port))
|
||||
}
|
||||
func dash() string {
|
||||
up := time.Since(startTime).Round(time.Second)
|
||||
svcs := []struct{n,s,l,i,c string}{{"API Gateway","Running","2ms","🌐","#00d4aa"},{"Database","Running","5ms","🗄️","#3b82f6"},{"Redis Cache","Running","<1ms","⚡","#f59e0b"},{"Message Queue","Running","3ms","📨","#8b5cf6"},{"Auth Service","Running","4ms","🔐","#ec4899"},{"Storage","Running","8ms","💾","#06b6d4"}}
|
||||
sh := ""; for _,s := range svcs { sh += fmt.Sprintf(`<div class="svc"><div class="si" style="background:%s18">%s</div><div class="sf"><div class="sn">%s</div><div class="ss" style="color:%s">● %s</div></div><div class="sl">%s</div></div>`,s.c,s.i,s.n,s.c,s.s,s.l) }
|
||||
bars := ""; days := []string{"Mon","Tue","Wed","Thu","Fri","Sat","Sun"}
|
||||
for i,d := range days { h:=30+rand.Intn(60); bars+=fmt.Sprintf(`<div class="bg"><div class="b" style="height:%d%%;animation-delay:%.2fs"></div><div class="bl">%s</div></div>`,h,0.3+float64(i)*0.08,d) }
|
||||
return fmt.Sprintf(`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>Echo Monitor</title><style>*{margin:0;padding:0;box-sizing:border-box}:root{--bg:#080816;--card:rgba(255,255,255,0.035);--border:rgba(255,255,255,0.07);--text:#f0f0ff;--text2:rgba(240,240,255,0.55);--text3:rgba(240,240,255,0.3);--cyan:#06b6d4;--radius:14px}body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}.c{max-width:460px;margin:0 auto;padding:0 14px 32px;position:relative;z-index:1}.hdr{padding:20px 2px;display:flex;justify-content:space-between;align-items:center}.hdr h1{font-size:22px;font-weight:800;background:linear-gradient(135deg,#06b6d4,#22d3ee);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.hdr p{font-size:12px;color:var(--text2)}.badge{font-size:11px;font-weight:600;padding:5px 12px;border-radius:20px;background:rgba(6,182,212,0.1);color:var(--cyan)}.row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px}.m{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center}.m-val{font-size:20px;font-weight:800}.m-label{font-size:10px;color:var(--text2);text-transform:uppercase}.m:nth-child(1) .m-val{color:#00d4aa}.m:nth-child(2) .m-val{color:var(--cyan)}.m:nth-child(3) .m-val{color:#8b5cf6}.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;margin-bottom:14px}.card-h{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}.card-t{font-size:15px;font-weight:700}.card-b{font-size:10px;font-weight:600;padding:3px 9px;border-radius:16px;background:rgba(6,182,212,0.1);color:var(--cyan)}.svc{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid rgba(255,255,255,0.03)}.svc:last-child{border:none}.si{width:36px;height:36px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}.sf{flex:1}.sn{font-size:13px;font-weight:600}.ss{font-size:11px}.sl{font-size:11px;color:var(--text3);font-family:monospace}.chart{display:flex;align-items:flex-end;justify-content:space-between;height:120px;gap:6px}.bg{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;height:100%%;justify-content:flex-end}.b{width:100%%;max-width:28px;border-radius:5px 5px 3px 3px;background:linear-gradient(180deg,#22d3ee,#0891b2);transform-origin:bottom;animation:grow .7s cubic-bezier(.34,1.56,.64,1) backwards}@keyframes grow{from{transform:scaleY(0)}to{transform:scaleY(1)}}.bl{font-size:9px;color:var(--text3)}.ft{text-align:center;padding:12px}.ft-b{font-size:10px;font-weight:600;color:var(--text3);padding:5px 12px;border-radius:20px;background:rgba(6,182,212,0.04)}@keyframes fadeIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}.m,.card{animation:fadeIn .5s ease backwards}</style></head><body>
|
||||
<div class="c"><div class="hdr"><div><h1>📡 Echo Monitor</h1><p>Service Monitoring Panel</p></div><div class="badge">All OK</div></div>
|
||||
<div class="row"><div class="m"><div class="m-val">%s</div><div class="m-label">Uptime</div></div><div class="m"><div class="m-val">6</div><div class="m-label">Services</div></div><div class="m"><div class="m-val">99.9%%</div><div class="m-label">Avail</div></div></div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">Services</span><span class="card-b">🟢 All Online</span></div>%s</div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">Traffic</span><span class="card-b">📊 Weekly</span></div><div class="chart">%s</div></div>
|
||||
<div class="ft"><div class="ft-b">Echo v4 · Go 1.22 · Running</div></div></div></body></html>`,up,sh,bars)
|
||||
}
|
||||
5
app/src/main/assets/sample_projects/go-echo/go.mod
Normal file
5
app/src/main/assets/sample_projects/go-echo/go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module github.com/demo/echo-server
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/labstack/echo/v4 v4.11.4
|
||||
117
app/src/main/assets/sample_projects/go-echo/main.go
Normal file
117
app/src/main/assets/sample_projects/go-echo/main.go
Normal file
@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
// Echo 服务监控面板 — 精美的 Go Web 应用示例
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
Service string `json:"service"`
|
||||
Status string `json:"status"`
|
||||
Uptime string `json:"uptime"`
|
||||
}
|
||||
|
||||
var startTime = time.Now()
|
||||
|
||||
func main() {
|
||||
e := echo.New()
|
||||
e.HideBanner = true
|
||||
e.Use(middleware.Logger())
|
||||
|
||||
e.GET("/", dashboardHandler)
|
||||
e.GET("/health", func(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, Status{"echo-dashboard", "healthy", time.Since(startTime).String()})
|
||||
})
|
||||
e.GET("/api/services", func(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"services": []map[string]interface{}{
|
||||
{"name": "API Gateway", "status": "running", "latency": "2ms"},
|
||||
{"name": "Database", "status": "running", "latency": "5ms"},
|
||||
{"name": "Cache", "status": "running", "latency": "<1ms"},
|
||||
{"name": "Queue", "status": "running", "latency": "3ms"},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "1323"
|
||||
}
|
||||
e.Logger.Fatal(e.Start(":" + port))
|
||||
}
|
||||
|
||||
func dashboardHandler(c echo.Context) error {
|
||||
uptime := time.Since(startTime).Round(time.Second)
|
||||
services := []struct{ name, status, latency, icon, color string }{
|
||||
{"API Gateway", "运行中", "2ms", "🌐", "#00d4aa"},
|
||||
{"Database", "运行中", "5ms", "🗄️", "#3b82f6"},
|
||||
{"Redis Cache", "运行中", "<1ms", "⚡", "#f59e0b"},
|
||||
{"Message Queue", "运行中", "3ms", "📨", "#8b5cf6"},
|
||||
{"Auth Service", "运行中", "4ms", "🔐", "#ec4899"},
|
||||
{"Storage", "运行中", "8ms", "💾", "#06b6d4"},
|
||||
}
|
||||
svcHTML := ""
|
||||
for _, s := range services {
|
||||
svcHTML += fmt.Sprintf(`<div class="svc"><div class="svc-i" style="background:%s18">%s</div><div class="svc-info"><div class="svc-n">%s</div><div class="svc-s" style="color:%s">● %s</div></div><div class="svc-l">%s</div></div>`, s.color, s.icon, s.name, s.color, s.status, s.latency)
|
||||
}
|
||||
bars := ""
|
||||
days := []string{"周一", "周二", "周三", "周四", "周五", "周六", "周日"}
|
||||
for i, d := range days {
|
||||
h := 30 + rand.Intn(60)
|
||||
bars += fmt.Sprintf(`<div class="bg"><div class="b" style="height:%d%%;animation-delay:%.2fs"></div><div class="bl">%s</div></div>`, h, 0.3+float64(i)*0.08, d)
|
||||
}
|
||||
|
||||
html := fmt.Sprintf(`<!DOCTYPE html>
|
||||
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>Echo Monitor</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{--bg:#080816;--card:rgba(255,255,255,0.035);--border:rgba(255,255,255,0.07);--text:#f0f0ff;--text2:rgba(240,240,255,0.55);--text3:rgba(240,240,255,0.3);--cyan:#06b6d4;--radius:14px}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
||||
body::before{content:'';position:fixed;top:-40%%;left:-40%%;width:180%%;height:180%%;background:radial-gradient(ellipse at 50%% 30%%,rgba(6,182,212,0.06) 0%%,transparent 55%%);pointer-events:none}
|
||||
.c{max-width:460px;margin:0 auto;padding:0 14px 32px;position:relative;z-index:1}
|
||||
.hdr{padding:20px 2px;display:flex;justify-content:space-between;align-items:center}
|
||||
.hdr h1{font-size:22px;font-weight:800;background:linear-gradient(135deg,#06b6d4,#22d3ee);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.hdr p{font-size:12px;color:var(--text2);margin-top:2px}
|
||||
.badge{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:5px 12px;border-radius:20px;background:rgba(6,182,212,0.1);color:var(--cyan);border:1px solid rgba(6,182,212,0.15)}
|
||||
.badge .dot{width:7px;height:7px;border-radius:50%%;background:#00d4aa;box-shadow:0 0 10px #00d4aa;animation:p 2s infinite}
|
||||
@keyframes p{0%%,100%%{opacity:1}50%%{opacity:.5}}
|
||||
.row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px}
|
||||
.m{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center}
|
||||
.m-val{font-size:20px;font-weight:800;margin-bottom:2px}
|
||||
.m-label{font-size:10px;color:var(--text2);text-transform:uppercase;letter-spacing:.5px;font-weight:500}
|
||||
.m:nth-child(1) .m-val{color:#00d4aa}.m:nth-child(2) .m-val{color:var(--cyan)}.m:nth-child(3) .m-val{color:#8b5cf6}
|
||||
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;margin-bottom:14px}
|
||||
.card-h{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}
|
||||
.card-t{font-size:15px;font-weight:700}.card-b{font-size:10px;font-weight:600;padding:3px 9px;border-radius:16px;background:rgba(6,182,212,0.1);color:var(--cyan)}
|
||||
.svc{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid rgba(255,255,255,0.03)}.svc:last-child{border:none}
|
||||
.svc-i{width:36px;height:36px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}
|
||||
.svc-info{flex:1}.svc-n{font-size:13px;font-weight:600}.svc-s{font-size:11px;font-weight:500}
|
||||
.svc-l{font-size:11px;color:var(--text3);font-family:monospace;font-weight:600}
|
||||
.chart{display:flex;align-items:flex-end;justify-content:space-between;height:120px;gap:6px}
|
||||
.bg{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;height:100%%;justify-content:flex-end}
|
||||
.b{width:100%%;max-width:28px;border-radius:5px 5px 3px 3px;background:linear-gradient(180deg,#22d3ee,#0891b2);box-shadow:0 0 8px rgba(6,182,212,0.2);transform-origin:bottom;animation:grow .7s cubic-bezier(.34,1.56,.64,1) backwards}
|
||||
@keyframes grow{from{transform:scaleY(0)}to{transform:scaleY(1)}}.bl{font-size:9px;color:var(--text3);font-weight:500}
|
||||
.ft{text-align:center;padding:12px}.ft-b{display:inline-flex;align-items:center;gap:6px;font-size:10px;font-weight:600;color:var(--text3);padding:5px 12px;border-radius:20px;background:rgba(6,182,212,0.04);border:1px solid rgba(6,182,212,0.08)}
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}.m,.card{animation:fadeIn .5s ease backwards}
|
||||
</style></head><body>
|
||||
<div class="c">
|
||||
<div class="hdr"><div><h1>📡 Echo Monitor</h1><p>服务监控面板</p></div><div class="badge"><span class="dot"></span>全部正常</div></div>
|
||||
<div class="row">
|
||||
<div class="m"><div class="m-val">%s</div><div class="m-label">运行时间</div></div>
|
||||
<div class="m"><div class="m-val">6</div><div class="m-label">服务数</div></div>
|
||||
<div class="m"><div class="m-val">99.9%%</div><div class="m-label">可用性</div></div>
|
||||
</div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">服务状态</span><span class="card-b">🟢 全部在线</span></div>%s</div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">流量趋势</span><span class="card-b">📊 本周</span></div><div class="chart">%s</div></div>
|
||||
<div class="ft"><div class="ft-b"><span class="dot" style="width:5px;height:5px;border-radius:50%%;background:#00d4aa;box-shadow:0 0 6px #00d4aa"></span>Echo v4 · Go 1.22 · 运行中</div></div>
|
||||
</div></body></html>`, uptime, svcHTML, bars)
|
||||
|
||||
return c.HTML(http.StatusOK, html)
|
||||
}
|
||||
5
app/src/main/assets/sample_projects/go-fiber-ar/go.mod
Normal file
5
app/src/main/assets/sample_projects/go-fiber-ar/go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module github.com/demo/fiber-app
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/gofiber/fiber/v2 v2.52.0
|
||||
26
app/src/main/assets/sample_projects/go-fiber-ar/main.go
Normal file
26
app/src/main/assets/sample_projects/go-fiber-ar/main.go
Normal file
@ -0,0 +1,26 @@
|
||||
package main
|
||||
// Fiber Performance Dashboard (AR)
|
||||
import ("fmt";"math/rand";"os";"runtime";"time";"github.com/gofiber/fiber/v2";"github.com/gofiber/fiber/v2/middleware/logger")
|
||||
var startTime = time.Now()
|
||||
func main() {
|
||||
app := fiber.New(fiber.Config{AppName:"Fiber Dashboard",DisableStartupMessage:true}); app.Use(logger.New())
|
||||
app.Get("/", func(c *fiber.Ctx) error { c.Set("Content-Type","text/html; charset=utf-8"); return c.SendString(dash()) })
|
||||
app.Get("/api/users", func(c *fiber.Ctx) error { return c.JSON([]fiber.Map{{"id":1,"name":"أحمد"},{"id":2,"name":"فاطمة"},{"id":3,"name":"محمد"}}) })
|
||||
app.Get("/api/health", func(c *fiber.Ctx) error { var m runtime.MemStats; runtime.ReadMemStats(&m); return c.JSON(fiber.Map{"status":"healthy","memory_mb":m.Alloc/1024/1024}) })
|
||||
port := os.Getenv("PORT"); if port == "" { port = "3000" }; app.Listen(":"+port)
|
||||
}
|
||||
func dash() string {
|
||||
up := time.Since(startTime).Round(time.Second); var m runtime.MemStats; runtime.ReadMemStats(&m); mb := m.Alloc/1024/1024; gr := runtime.NumGoroutine()
|
||||
bars := ""; days := []string{"إثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت","أحد"}
|
||||
for i,d := range days { h:=25+rand.Intn(65); bars+=fmt.Sprintf(`<div style="flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;height:100%%;justify-content:flex-end"><div style="width:100%%;max-width:28px;height:%d%%;border-radius:5px 5px 3px 3px;background:linear-gradient(180deg,#2dd4bf,#0d9488);transform-origin:bottom;animation:g .7s cubic-bezier(.34,1.56,.64,1) backwards;animation-delay:%.2fs"></div><div style="font-size:9px;color:rgba(240,240,255,0.3)">%s</div></div>`,h,0.3+float64(i)*0.08,d) }
|
||||
mets := []struct{l,v,i,c string}{{"زمن الاستجابة","<1ms","⚡","#f59e0b"},{"الذاكرة",fmt.Sprintf("%dMB",mb),"💾","#8b5cf6"},{"الخيوط",fmt.Sprintf("%d",gr),"🔄","#06b6d4"},{"QPS",fmt.Sprintf("%d",1000+rand.Intn(4000)),"📊","#ec4899"}}
|
||||
mh := ""; for _,mt := range mets { mh += fmt.Sprintf(`<div style="display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid rgba(255,255,255,0.03)"><div style="width:36px;height:36px;border-radius:10px;background:%s18;display:flex;align-items:center;justify-content:center;font-size:16px">%s</div><div style="flex:1"><div style="font-size:12px;color:rgba(240,240,255,0.55)">%s</div><div style="font-size:16px;font-weight:700;color:%s">%s</div></div></div>`,mt.c,mt.i,mt.l,mt.c,mt.v) }
|
||||
return fmt.Sprintf(`<!DOCTYPE html><html lang="ar" dir="rtl"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>Fiber</title><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Tahoma,sans-serif;background:#080816;color:#f0f0ff;min-height:100vh;direction:rtl}@keyframes g{from{transform:scaleY(0)}to{transform:scaleY(1)}}</style></head><body>
|
||||
<div style="max-width:460px;margin:0 auto;padding:0 14px 32px">
|
||||
<div style="padding:20px 2px;display:flex;justify-content:space-between;align-items:center"><div><h1 style="font-size:22px;font-weight:800;background:linear-gradient(135deg,#14b8a6,#2dd4bf);-webkit-background-clip:text;-webkit-text-fill-color:transparent">🔥 Fiber لوحة</h1><p style="font-size:12px;color:rgba(240,240,255,0.55)">إطار عمل Go فائق السرعة</p></div><div style="font-size:11px;font-weight:600;padding:5px 12px;border-radius:20px;background:rgba(20,184,166,0.1);color:#14b8a6">يعمل</div></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px"><div style="background:rgba(255,255,255,0.035);border:1px solid rgba(255,255,255,0.07);border-radius:14px;padding:14px;text-align:center"><div style="font-size:20px;font-weight:800;color:#00d4aa">%s</div><div style="font-size:10px;color:rgba(240,240,255,0.55)">التشغيل</div></div><div style="background:rgba(255,255,255,0.035);border:1px solid rgba(255,255,255,0.07);border-radius:14px;padding:14px;text-align:center"><div style="font-size:20px;font-weight:800;color:#14b8a6">%dMB</div><div style="font-size:10px;color:rgba(240,240,255,0.55)">الذاكرة</div></div><div style="background:rgba(255,255,255,0.035);border:1px solid rgba(255,255,255,0.07);border-radius:14px;padding:14px;text-align:center"><div style="font-size:20px;font-weight:800;color:#8b5cf6">%d</div><div style="font-size:10px;color:rgba(240,240,255,0.55)">الخيوط</div></div></div>
|
||||
<div style="background:rgba(255,255,255,0.035);border:1px solid rgba(255,255,255,0.07);border-radius:14px;padding:18px;margin-bottom:14px"><div style="font-size:15px;font-weight:700;margin-bottom:14px">مقاييس الأداء</div>%s</div>
|
||||
<div style="background:rgba(255,255,255,0.035);border:1px solid rgba(255,255,255,0.07);border-radius:14px;padding:18px;margin-bottom:14px"><div style="font-size:15px;font-weight:700;margin-bottom:14px">الإنتاجية</div><div style="display:flex;align-items:flex-end;justify-content:space-between;height:120px;gap:6px;direction:ltr">%s</div></div>
|
||||
<div style="text-align:center;padding:12px;font-size:10px;color:rgba(240,240,255,0.3)">Fiber v2 · Go · يعمل</div>
|
||||
</div></body></html>`,up,mb,gr,mh,bars)
|
||||
}
|
||||
5
app/src/main/assets/sample_projects/go-fiber-en/go.mod
Normal file
5
app/src/main/assets/sample_projects/go-fiber-en/go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module github.com/demo/fiber-app
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/gofiber/fiber/v2 v2.52.0
|
||||
26
app/src/main/assets/sample_projects/go-fiber-en/main.go
Normal file
26
app/src/main/assets/sample_projects/go-fiber-en/main.go
Normal file
@ -0,0 +1,26 @@
|
||||
package main
|
||||
// Fiber Performance Dashboard (EN)
|
||||
import ("fmt";"math/rand";"os";"runtime";"time";"github.com/gofiber/fiber/v2";"github.com/gofiber/fiber/v2/middleware/logger")
|
||||
var startTime = time.Now()
|
||||
func main() {
|
||||
app := fiber.New(fiber.Config{AppName:"Fiber Dashboard",DisableStartupMessage:true}); app.Use(logger.New())
|
||||
app.Get("/", func(c *fiber.Ctx) error { c.Set("Content-Type","text/html; charset=utf-8"); return c.SendString(dash()) })
|
||||
app.Get("/api/users", func(c *fiber.Ctx) error { return c.JSON([]fiber.Map{{"id":1,"name":"Alice"},{"id":2,"name":"Bob"},{"id":3,"name":"Carol"}}) })
|
||||
app.Get("/api/health", func(c *fiber.Ctx) error { var m runtime.MemStats; runtime.ReadMemStats(&m); return c.JSON(fiber.Map{"status":"healthy","memory_mb":m.Alloc/1024/1024,"goroutines":runtime.NumGoroutine()}) })
|
||||
port := os.Getenv("PORT"); if port == "" { port = "3000" }; app.Listen(":"+port)
|
||||
}
|
||||
func dash() string {
|
||||
up := time.Since(startTime).Round(time.Second); var m runtime.MemStats; runtime.ReadMemStats(&m); mb := m.Alloc/1024/1024; gr := runtime.NumGoroutine()
|
||||
bars := ""; days := []string{"Mon","Tue","Wed","Thu","Fri","Sat","Sun"}
|
||||
for i,d := range days { h:=25+rand.Intn(65); bars+=fmt.Sprintf(`<div class="bg"><div class="b" style="height:%d%%;animation-delay:%.2fs"></div><div class="bl">%s</div></div>`,h,0.3+float64(i)*0.08,d) }
|
||||
mets := []struct{l,v,i,c string}{{"Response","<1ms","⚡","#f59e0b"},{"Memory",fmt.Sprintf("%dMB",mb),"💾","#8b5cf6"},{"Goroutines",fmt.Sprintf("%d",gr),"🔄","#06b6d4"},{"QPS",fmt.Sprintf("%d",1000+rand.Intn(4000)),"📊","#ec4899"}}
|
||||
mh := ""; for _,mt := range mets { mh += fmt.Sprintf(`<div class="pf"><div class="pi" style="background:%s18">%s</div><div class="px"><div class="pl">%s</div><div class="pv" style="color:%s">%s</div></div></div>`,mt.c,mt.i,mt.l,mt.c,mt.v) }
|
||||
return fmt.Sprintf(`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>Fiber</title><style>*{margin:0;padding:0;box-sizing:border-box}:root{--bg:#080816;--card:rgba(255,255,255,0.035);--border:rgba(255,255,255,0.07);--text:#f0f0ff;--text2:rgba(240,240,255,0.55);--text3:rgba(240,240,255,0.3);--teal:#14b8a6;--radius:14px}body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}.c{max-width:460px;margin:0 auto;padding:0 14px 32px;position:relative;z-index:1}.hdr{padding:20px 2px;display:flex;justify-content:space-between;align-items:center}.hdr h1{font-size:22px;font-weight:800;background:linear-gradient(135deg,#14b8a6,#2dd4bf);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.hdr p{font-size:12px;color:var(--text2)}.badge{font-size:11px;font-weight:600;padding:5px 12px;border-radius:20px;background:rgba(20,184,166,0.1);color:var(--teal)}.row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px}.m{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center}.m-val{font-size:20px;font-weight:800}.m-label{font-size:10px;color:var(--text2);text-transform:uppercase}.m:nth-child(1) .m-val{color:#00d4aa}.m:nth-child(2) .m-val{color:var(--teal)}.m:nth-child(3) .m-val{color:#8b5cf6}.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;margin-bottom:14px}.card-h{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}.card-t{font-size:15px;font-weight:700}.card-b{font-size:10px;font-weight:600;padding:3px 9px;border-radius:16px;background:rgba(20,184,166,0.1);color:var(--teal)}.pf{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid rgba(255,255,255,0.03)}.pf:last-child{border:none}.pi{width:36px;height:36px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}.px{flex:1}.pl{font-size:12px;color:var(--text2)}.pv{font-size:16px;font-weight:700}.chart{display:flex;align-items:flex-end;justify-content:space-between;height:120px;gap:6px}.bg{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;height:100%%;justify-content:flex-end}.b{width:100%%;max-width:28px;border-radius:5px 5px 3px 3px;background:linear-gradient(180deg,#2dd4bf,#0d9488);transform-origin:bottom;animation:grow .7s cubic-bezier(.34,1.56,.64,1) backwards}@keyframes grow{from{transform:scaleY(0)}to{transform:scaleY(1)}}.bl{font-size:9px;color:var(--text3)}.try{background:rgba(20,184,166,0.1);color:var(--teal);border:1px solid rgba(20,184,166,0.2);padding:6px 14px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer}.result{margin-top:10px;padding:12px;background:rgba(0,0,0,0.3);border-radius:8px;font-family:monospace;font-size:11px;color:#00d4aa;max-height:180px;overflow-y:auto;display:none;white-space:pre-wrap}.ft{text-align:center;padding:12px}.ft-b{font-size:10px;font-weight:600;color:var(--text3);padding:5px 12px;border-radius:20px;background:rgba(20,184,166,0.04)}@keyframes fadeIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}.m,.card{animation:fadeIn .5s ease backwards}</style></head><body>
|
||||
<div class="c"><div class="hdr"><div><h1>🔥 Fiber Dashboard</h1><p>Ultra-Fast Go Web Framework</p></div><div class="badge">Running</div></div>
|
||||
<div class="row"><div class="m"><div class="m-val">%s</div><div class="m-label">Uptime</div></div><div class="m"><div class="m-val">%dMB</div><div class="m-label">Memory</div></div><div class="m"><div class="m-val">%d</div><div class="m-label">Goroutines</div></div></div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">Performance</span><span class="card-b">⚡ Live</span></div>%s</div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">Throughput</span><span class="card-b">📊 Weekly</span></div><div class="chart">%s</div></div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">Live Test</span><span class="card-b">🧪</span></div><div style="display:flex;gap:8px"><button class="try" onclick="tryApi('/api/users')">👥 Users</button><button class="try" onclick="tryApi('/api/health')">💚 Health</button></div><div class="result" id="r"></div></div>
|
||||
<div class="ft"><div class="ft-b">Fiber v2 · Go %s · Running</div></div></div>
|
||||
<script>function tryApi(u){var r=document.getElementById('r');r.style.display='block';r.textContent='⏳';fetch(u).then(function(x){return x.json()}).then(function(d){r.textContent=JSON.stringify(d,null,2)}).catch(function(e){r.textContent='❌ '+e.message})}</script></body></html>`,up,mb,gr,mh,bars,runtime.Version())
|
||||
}
|
||||
5
app/src/main/assets/sample_projects/go-fiber/go.mod
Normal file
5
app/src/main/assets/sample_projects/go-fiber/go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module github.com/demo/fiber-app
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/gofiber/fiber/v2 v2.52.0
|
||||
126
app/src/main/assets/sample_projects/go-fiber/main.go
Normal file
126
app/src/main/assets/sample_projects/go-fiber/main.go
Normal file
@ -0,0 +1,126 @@
|
||||
package main
|
||||
|
||||
// Fiber 性能分析面板 — 精美的 Go Web 应用示例
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
)
|
||||
|
||||
var startTime = time.Now()
|
||||
|
||||
func main() {
|
||||
app := fiber.New(fiber.Config{AppName: "Fiber Dashboard v2.0", DisableStartupMessage: true})
|
||||
app.Use(logger.New())
|
||||
|
||||
app.Get("/", dashboardHandler)
|
||||
app.Get("/api/users", func(c *fiber.Ctx) error {
|
||||
return c.JSON([]fiber.Map{
|
||||
{"id": 1, "name": "Alice", "role": "Admin"},
|
||||
{"id": 2, "name": "Bob", "role": "User"},
|
||||
{"id": 3, "name": "Carol", "role": "Editor"},
|
||||
})
|
||||
})
|
||||
app.Get("/api/health", func(c *fiber.Ctx) error {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
return c.JSON(fiber.Map{
|
||||
"status": "healthy",
|
||||
"framework": "Fiber",
|
||||
"uptime": time.Since(startTime).String(),
|
||||
"memory_mb": m.Alloc / 1024 / 1024,
|
||||
"goroutines": runtime.NumGoroutine(),
|
||||
})
|
||||
})
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "3000"
|
||||
}
|
||||
app.Listen(":" + port)
|
||||
}
|
||||
|
||||
func dashboardHandler(c *fiber.Ctx) error {
|
||||
uptime := time.Since(startTime).Round(time.Second)
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
memMB := m.Alloc / 1024 / 1024
|
||||
goroutines := runtime.NumGoroutine()
|
||||
|
||||
bars := ""
|
||||
days := []string{"周一", "周二", "周三", "周四", "周五", "周六", "周日"}
|
||||
for i, d := range days {
|
||||
h := 25 + rand.Intn(65)
|
||||
bars += fmt.Sprintf(`<div class="bg"><div class="b" style="height:%d%%;animation-delay:%.2fs"></div><div class="bl">%s</div></div>`, h, 0.3+float64(i)*0.08, d)
|
||||
}
|
||||
|
||||
metrics := []struct{ label, value, icon, color string }{
|
||||
{"响应时间", "<1ms", "⚡", "#f59e0b"},
|
||||
{"内存使用", fmt.Sprintf("%dMB", memMB), "💾", "#8b5cf6"},
|
||||
{"协程数", fmt.Sprintf("%d", goroutines), "🔄", "#06b6d4"},
|
||||
{"QPS", fmt.Sprintf("%d", 1000+rand.Intn(4000)), "📊", "#ec4899"},
|
||||
}
|
||||
metricHTML := ""
|
||||
for _, met := range metrics {
|
||||
metricHTML += fmt.Sprintf(`<div class="perf"><div class="perf-i" style="background:%s18">%s</div><div class="perf-info"><div class="perf-l">%s</div><div class="perf-v" style="color:%s">%s</div></div></div>`, met.color, met.icon, met.label, met.color, met.value)
|
||||
}
|
||||
|
||||
html := fmt.Sprintf(`<!DOCTYPE html>
|
||||
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>Fiber Dashboard</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{--bg:#080816;--card:rgba(255,255,255,0.035);--border:rgba(255,255,255,0.07);--text:#f0f0ff;--text2:rgba(240,240,255,0.55);--text3:rgba(240,240,255,0.3);--teal:#14b8a6;--radius:14px}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
||||
body::before{content:'';position:fixed;top:-40%%;left:-40%%;width:180%%;height:180%%;background:radial-gradient(ellipse at 50%% 30%%,rgba(20,184,166,0.06) 0%%,transparent 55%%);pointer-events:none}
|
||||
.c{max-width:460px;margin:0 auto;padding:0 14px 32px;position:relative;z-index:1}
|
||||
.hdr{padding:20px 2px;display:flex;justify-content:space-between;align-items:center}
|
||||
.hdr h1{font-size:22px;font-weight:800;background:linear-gradient(135deg,#14b8a6,#2dd4bf);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.hdr p{font-size:12px;color:var(--text2);margin-top:2px}
|
||||
.badge{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:5px 12px;border-radius:20px;background:rgba(20,184,166,0.1);color:var(--teal);border:1px solid rgba(20,184,166,0.15)}
|
||||
.badge .dot{width:7px;height:7px;border-radius:50%%;background:#00d4aa;box-shadow:0 0 10px #00d4aa;animation:p 2s infinite}
|
||||
@keyframes p{0%%,100%%{opacity:1}50%%{opacity:.5}}
|
||||
.row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px}
|
||||
.m{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center}
|
||||
.m-val{font-size:20px;font-weight:800;margin-bottom:2px}
|
||||
.m-label{font-size:10px;color:var(--text2);text-transform:uppercase;letter-spacing:.5px;font-weight:500}
|
||||
.m:nth-child(1) .m-val{color:#00d4aa}.m:nth-child(2) .m-val{color:var(--teal)}.m:nth-child(3) .m-val{color:#8b5cf6}
|
||||
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;margin-bottom:14px}
|
||||
.card-h{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}
|
||||
.card-t{font-size:15px;font-weight:700}.card-b{font-size:10px;font-weight:600;padding:3px 9px;border-radius:16px;background:rgba(20,184,166,0.1);color:var(--teal)}
|
||||
.perf{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid rgba(255,255,255,0.03)}.perf:last-child{border:none}
|
||||
.perf-i{width:36px;height:36px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}
|
||||
.perf-info{flex:1}.perf-l{font-size:12px;color:var(--text2)}.perf-v{font-size:16px;font-weight:700}
|
||||
.chart{display:flex;align-items:flex-end;justify-content:space-between;height:120px;gap:6px}
|
||||
.bg{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;height:100%%;justify-content:flex-end}
|
||||
.b{width:100%%;max-width:28px;border-radius:5px 5px 3px 3px;background:linear-gradient(180deg,#2dd4bf,#0d9488);box-shadow:0 0 8px rgba(20,184,166,0.2);transform-origin:bottom;animation:grow .7s cubic-bezier(.34,1.56,.64,1) backwards}
|
||||
@keyframes grow{from{transform:scaleY(0)}to{transform:scaleY(1)}}.bl{font-size:9px;color:var(--text3);font-weight:500}
|
||||
.try{background:rgba(20,184,166,0.1);color:var(--teal);border:1px solid rgba(20,184,166,0.2);padding:6px 14px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer}.try:active{transform:scale(.95)}
|
||||
.result{margin-top:10px;padding:12px;background:rgba(0,0,0,0.3);border-radius:8px;font-family:monospace;font-size:11px;color:#00d4aa;max-height:180px;overflow-y:auto;display:none;white-space:pre-wrap;word-break:break-all}
|
||||
.ft{text-align:center;padding:12px}.ft-b{display:inline-flex;align-items:center;gap:6px;font-size:10px;font-weight:600;color:var(--text3);padding:5px 12px;border-radius:20px;background:rgba(20,184,166,0.04);border:1px solid rgba(20,184,166,0.08)}
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}.m,.card{animation:fadeIn .5s ease backwards}
|
||||
</style></head><body>
|
||||
<div class="c">
|
||||
<div class="hdr"><div><h1>🔥 Fiber Dashboard</h1><p>超快 Go Web 框架</p></div><div class="badge"><span class="dot"></span>运行中</div></div>
|
||||
<div class="row">
|
||||
<div class="m"><div class="m-val">%s</div><div class="m-label">运行时间</div></div>
|
||||
<div class="m"><div class="m-val">%dMB</div><div class="m-label">内存</div></div>
|
||||
<div class="m"><div class="m-val">%d</div><div class="m-label">协程</div></div>
|
||||
</div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">性能指标</span><span class="card-b">⚡ 实时</span></div>%s</div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">吞吐趋势</span><span class="card-b">📊 本周</span></div><div class="chart">%s</div></div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">在线测试</span><span class="card-b">🧪 交互</span></div>
|
||||
<div style="display:flex;gap:8px"><button class="try" onclick="tryApi('/api/users')">👥 用户列表</button><button class="try" onclick="tryApi('/api/health')">💚 健康检查</button></div>
|
||||
<div class="result" id="r"></div></div>
|
||||
<div class="ft"><div class="ft-b"><span class="dot" style="width:5px;height:5px;border-radius:50%%;background:#00d4aa;box-shadow:0 0 6px #00d4aa"></span>Fiber v2 · Go %s · 运行中</div></div>
|
||||
</div>
|
||||
<script>function tryApi(u){var r=document.getElementById('r');r.style.display='block';r.textContent='⏳ ...';fetch(u).then(function(x){return x.json()}).then(function(d){r.textContent=JSON.stringify(d,null,2)}).catch(function(e){r.textContent='❌ '+e.message})}</script>
|
||||
</body></html>`, uptime, memMB, goroutines, metricHTML, bars, runtime.Version())
|
||||
|
||||
c.Set("Content-Type", "text/html; charset=utf-8")
|
||||
return c.SendString(html)
|
||||
}
|
||||
5
app/src/main/assets/sample_projects/go-gin-ar/go.mod
Normal file
5
app/src/main/assets/sample_projects/go-gin-ar/go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module github.com/demo/gin-api
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/gin-gonic/gin v1.9.1
|
||||
25
app/src/main/assets/sample_projects/go-gin-ar/main.go
Normal file
25
app/src/main/assets/sample_projects/go-gin-ar/main.go
Normal file
@ -0,0 +1,25 @@
|
||||
package main
|
||||
// Gin REST API Dashboard — Go Web (AR)
|
||||
import ("fmt";"math/rand";"net/http";"os";"time";"github.com/gin-gonic/gin")
|
||||
type Item struct { ID int `json:"id"`; Name string `json:"name"`; Price float64 `json:"price"` }
|
||||
var items = []Item{{1,"MacBook Pro",1999},{2,"iPhone 15",999},{3,"AirPods Pro",249},{4,"iPad Air",599}}
|
||||
var startTime = time.Now()
|
||||
func main() {
|
||||
gin.SetMode(gin.ReleaseMode); r := gin.Default()
|
||||
r.GET("/", func(c *gin.Context) { c.Header("Content-Type","text/html; charset=utf-8"); c.String(http.StatusOK, dash()) })
|
||||
r.GET("/api/items", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data":items}) })
|
||||
r.GET("/api/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status":"healthy","uptime":time.Since(startTime).String()}) })
|
||||
port := os.Getenv("PORT"); if port == "" { port = "8080" }; r.Run(":"+port)
|
||||
}
|
||||
func dash() string {
|
||||
up := time.Since(startTime).Round(time.Second); bars := ""
|
||||
days := []string{"إثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت","أحد"}
|
||||
for i,d := range days { h:=20+rand.Intn(70); bars+=fmt.Sprintf(`<div class="bg"><div class="b" style="height:%d%%;animation-delay:%.2fs"></div><div class="bl">%s</div></div>`,h,0.3+float64(i)*0.08,d) }
|
||||
return fmt.Sprintf(`<!DOCTYPE html><html lang="ar" dir="rtl"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>Gin</title><style>*{margin:0;padding:0;box-sizing:border-box}:root{--bg:#080818;--card:rgba(255,255,255,0.035);--border:rgba(255,255,255,0.07);--text:#f0f0ff;--text2:rgba(240,240,255,0.55);--text3:rgba(240,240,255,0.3);--blue:#00b4d8;--radius:14px}body{font-family:-apple-system,BlinkMacSystemFont,Tahoma,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;direction:rtl}.c{max-width:460px;margin:0 auto;padding:0 14px 32px;position:relative;z-index:1}.hdr{padding:20px 2px;display:flex;justify-content:space-between;align-items:center}.hdr h1{font-size:22px;font-weight:800;background:linear-gradient(135deg,#00b4d8,#48cae4);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.hdr p{font-size:12px;color:var(--text2)}.badge{font-size:11px;font-weight:600;padding:5px 12px;border-radius:20px;background:rgba(0,180,216,0.1);color:var(--blue)}.row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px}.m{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center}.m-val{font-size:20px;font-weight:800}.m-label{font-size:10px;color:var(--text2)}.m:nth-child(1) .m-val{color:#00d4aa}.m:nth-child(2) .m-val{color:var(--blue)}.m:nth-child(3) .m-val{color:#8b5cf6}.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;margin-bottom:14px}.card-h{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}.card-t{font-size:15px;font-weight:700}.card-b{font-size:10px;font-weight:600;padding:3px 9px;border-radius:16px;background:rgba(0,180,216,0.1);color:var(--blue)}.chart{display:flex;align-items:flex-end;justify-content:space-between;height:120px;gap:6px;direction:ltr}.bg{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;height:100%%;justify-content:flex-end}.b{width:100%%;max-width:28px;border-radius:5px 5px 3px 3px;background:linear-gradient(180deg,#48cae4,#0096c7);transform-origin:bottom;animation:grow .7s cubic-bezier(.34,1.56,.64,1) backwards}@keyframes grow{from{transform:scaleY(0)}to{transform:scaleY(1)}}.bl{font-size:9px;color:var(--text3)}.try{background:rgba(0,180,216,0.1);color:var(--blue);border:1px solid rgba(0,180,216,0.2);padding:6px 14px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer}.result{margin-top:10px;padding:12px;background:rgba(0,0,0,0.3);border-radius:8px;font-family:monospace;font-size:11px;color:#00d4aa;max-height:180px;overflow-y:auto;display:none;white-space:pre-wrap}.ft{text-align:center;padding:12px}.ft-b{font-size:10px;font-weight:600;color:var(--text3);padding:5px 12px;border-radius:20px;background:rgba(0,180,216,0.04)}@keyframes fadeIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}.m,.card{animation:fadeIn .5s ease backwards}</style></head><body>
|
||||
<div class="c"><div class="hdr"><div><h1>🚀 Gin لوحة</h1><p>إطار عمل Go عالي الأداء</p></div><div class="badge">يعمل</div></div>
|
||||
<div class="row"><div class="m"><div class="m-val">%s</div><div class="m-label">وقت التشغيل</div></div><div class="m"><div class="m-val">%d</div><div class="m-label">المنتجات</div></div><div class="m"><div class="m-val">4</div><div class="m-label">المسارات</div></div></div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">اتجاه الزيارات</span><span class="card-b">📊 أسبوعي</span></div><div class="chart">%s</div></div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">اختبار مباشر</span><span class="card-b">🧪</span></div><div style="display:flex;gap:8px"><button class="try" onclick="tryApi('/api/items')">📦 المنتجات</button><button class="try" onclick="tryApi('/api/health')">💚 الصحة</button></div><div class="result" id="r"></div></div>
|
||||
<div class="ft"><div class="ft-b">Gin · Go 1.22 · يعمل</div></div></div>
|
||||
<script>function tryApi(u){var r=document.getElementById('r');r.style.display='block';r.textContent='⏳';fetch(u).then(function(x){return x.json()}).then(function(d){r.textContent=JSON.stringify(d,null,2)}).catch(function(e){r.textContent='❌ '+e.message})}</script></body></html>`,up,len(items),bars)
|
||||
}
|
||||
5
app/src/main/assets/sample_projects/go-gin-en/go.mod
Normal file
5
app/src/main/assets/sample_projects/go-gin-en/go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module github.com/demo/gin-api
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/gin-gonic/gin v1.9.1
|
||||
52
app/src/main/assets/sample_projects/go-gin-en/main.go
Normal file
52
app/src/main/assets/sample_projects/go-gin-en/main.go
Normal file
@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
// Gin REST API Dashboard — Go Web App Demo (EN)
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Item struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Price float64 `json:"price"`
|
||||
}
|
||||
|
||||
var items = []Item{{1, "MacBook Pro", 1999.00}, {2, "iPhone 15", 999.00}, {3, "AirPods Pro", 249.00}, {4, "iPad Air", 599.00}}
|
||||
var startTime = time.Now()
|
||||
|
||||
func main() {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.Default()
|
||||
r.GET("/", func(c *gin.Context) { c.Header("Content-Type", "text/html; charset=utf-8"); c.String(http.StatusOK, dashboard()) })
|
||||
api := r.Group("/api")
|
||||
{
|
||||
api.GET("/items", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok", "data": items}) })
|
||||
api.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "healthy", "uptime": time.Since(startTime).String()}) })
|
||||
}
|
||||
port := os.Getenv("PORT"); if port == "" { port = "8080" }
|
||||
r.Run(":" + port)
|
||||
}
|
||||
|
||||
func dashboard() string {
|
||||
up := time.Since(startTime).Round(time.Second)
|
||||
bars := ""
|
||||
days := []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}
|
||||
for i, d := range days {
|
||||
h := 20 + rand.Intn(70)
|
||||
bars += fmt.Sprintf(`<div class="bg"><div class="b" style="height:%d%%;animation-delay:%.2fs"></div><div class="bl">%s</div></div>`, h, 0.3+float64(i)*0.08, d)
|
||||
}
|
||||
return fmt.Sprintf(`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>Gin Dashboard</title>
|
||||
<style>*{margin:0;padding:0;box-sizing:border-box}:root{--bg:#080818;--card:rgba(255,255,255,0.035);--border:rgba(255,255,255,0.07);--text:#f0f0ff;--text2:rgba(240,240,255,0.55);--text3:rgba(240,240,255,0.3);--blue:#00b4d8;--radius:14px}body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}body::before{content:'';position:fixed;top:-40%%;left:-40%%;width:180%%;height:180%%;background:radial-gradient(ellipse at 50%% 30%%,rgba(0,180,216,0.06) 0%%,transparent 55%%);pointer-events:none}.c{max-width:460px;margin:0 auto;padding:0 14px 32px;position:relative;z-index:1}.hdr{padding:20px 2px;display:flex;justify-content:space-between;align-items:center}.hdr h1{font-size:22px;font-weight:800;background:linear-gradient(135deg,#00b4d8,#48cae4);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.hdr p{font-size:12px;color:var(--text2);margin-top:2px}.badge{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:5px 12px;border-radius:20px;background:rgba(0,180,216,0.1);color:var(--blue);border:1px solid rgba(0,180,216,0.15)}.badge .dot{width:7px;height:7px;border-radius:50%%;background:var(--blue);box-shadow:0 0 10px var(--blue);animation:p 2s infinite}@keyframes p{0%%,100%%{opacity:1}50%%{opacity:.5}}.row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px}.m{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center}.m-val{font-size:20px;font-weight:800;margin-bottom:2px}.m-label{font-size:10px;color:var(--text2);text-transform:uppercase;letter-spacing:.5px}.m:nth-child(1) .m-val{color:#00d4aa}.m:nth-child(2) .m-val{color:var(--blue)}.m:nth-child(3) .m-val{color:#8b5cf6}.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;margin-bottom:14px}.card-h{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}.card-t{font-size:15px;font-weight:700}.card-b{font-size:10px;font-weight:600;padding:3px 9px;border-radius:16px;background:rgba(0,180,216,0.1);color:var(--blue)}.chart{display:flex;align-items:flex-end;justify-content:space-between;height:130px;gap:6px}.bg{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;height:100%%;justify-content:flex-end}.b{width:100%%;max-width:28px;border-radius:5px 5px 3px 3px;background:linear-gradient(180deg,#48cae4,#0096c7);box-shadow:0 0 10px rgba(0,180,216,0.2);transform-origin:bottom;animation:grow .7s cubic-bezier(.34,1.56,.64,1) backwards}@keyframes grow{from{transform:scaleY(0)}to{transform:scaleY(1)}}.bl{font-size:9px;color:var(--text3);font-weight:500}.try{background:rgba(0,180,216,0.1);color:var(--blue);border:1px solid rgba(0,180,216,0.2);padding:6px 14px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer}.result{margin-top:10px;padding:12px;background:rgba(0,0,0,0.3);border-radius:8px;font-family:monospace;font-size:11px;color:#00d4aa;max-height:180px;overflow-y:auto;display:none;white-space:pre-wrap;word-break:break-all}.ft{text-align:center;padding:12px}.ft-b{display:inline-flex;align-items:center;gap:6px;font-size:10px;font-weight:600;color:var(--text3);padding:5px 12px;border-radius:20px;background:rgba(0,180,216,0.04);border:1px solid rgba(0,180,216,0.08)}@keyframes fadeIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}.m,.card{animation:fadeIn .5s ease backwards}</style></head><body>
|
||||
<div class="c"><div class="hdr"><div><h1>🚀 Gin Dashboard</h1><p>High-Performance Go Web Framework</p></div><div class="badge"><span class="dot"></span>Running</div></div>
|
||||
<div class="row"><div class="m"><div class="m-val">%s</div><div class="m-label">Uptime</div></div><div class="m"><div class="m-val">%d</div><div class="m-label">Items</div></div><div class="m"><div class="m-val">4</div><div class="m-label">Routes</div></div></div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">Traffic Trends</span><span class="card-b">📊 Weekly</span></div><div class="chart">%s</div></div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">Live Testing</span><span class="card-b">🧪 Try It</span></div><div style="display:flex;gap:8px"><button class="try" onclick="tryApi('/api/items')">📦 Items</button><button class="try" onclick="tryApi('/api/health')">💚 Health</button></div><div class="result" id="r"></div></div>
|
||||
<div class="ft"><div class="ft-b"><span class="dot" style="width:5px;height:5px;border-radius:50%%;background:var(--blue);box-shadow:0 0 6px var(--blue)"></span>Gin · Go 1.22 · Running</div></div></div>
|
||||
<script>function tryApi(u){var r=document.getElementById('r');r.style.display='block';r.textContent='⏳ ...';fetch(u).then(function(x){return x.json()}).then(function(d){r.textContent=JSON.stringify(d,null,2)}).catch(function(e){r.textContent='❌ '+e.message})}</script>
|
||||
</body></html>`, up, len(items), bars)
|
||||
}
|
||||
5
app/src/main/assets/sample_projects/go-gin/go.mod
Normal file
5
app/src/main/assets/sample_projects/go-gin/go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module github.com/demo/gin-api
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/gin-gonic/gin v1.9.1
|
||||
137
app/src/main/assets/sample_projects/go-gin/main.go
Normal file
137
app/src/main/assets/sample_projects/go-gin/main.go
Normal file
@ -0,0 +1,137 @@
|
||||
package main
|
||||
|
||||
// Gin REST API 仪表盘 — 精美的 Go Web 应用示例
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Item struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Price float64 `json:"price"`
|
||||
}
|
||||
|
||||
var items = []Item{
|
||||
{1, "MacBook Pro", 1999.00},
|
||||
{2, "iPhone 15", 999.00},
|
||||
{3, "AirPods Pro", 249.00},
|
||||
{4, "iPad Air", 599.00},
|
||||
}
|
||||
|
||||
var startTime = time.Now()
|
||||
|
||||
func main() {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.Default()
|
||||
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.String(http.StatusOK, getDashboardHTML())
|
||||
})
|
||||
|
||||
api := r.Group("/api")
|
||||
{
|
||||
api.GET("/items", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "data": items, "count": len(items)})
|
||||
})
|
||||
api.GET("/items/:id", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "data": items[0]})
|
||||
})
|
||||
api.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "healthy",
|
||||
"framework": "Gin",
|
||||
"uptime": time.Since(startTime).String(),
|
||||
"go": "1.22",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
r.Run(":" + port)
|
||||
}
|
||||
|
||||
func getDashboardHTML() string {
|
||||
uptime := time.Since(startTime).Round(time.Second)
|
||||
bars := ""
|
||||
days := []string{"周一", "周二", "周三", "周四", "周五", "周六", "周日"}
|
||||
for i, d := range days {
|
||||
h := 20 + rand.Intn(70)
|
||||
delay := fmt.Sprintf("%.2f", 0.3+float64(i)*0.08)
|
||||
bars += fmt.Sprintf(`<div class="bg"><div class="b" style="height:%d%%;animation-delay:%ss"></div><div class="bl">%s</div></div>`, h, delay, d)
|
||||
}
|
||||
|
||||
routes := []struct{ method, path, desc, ms string }{
|
||||
{"GET", "/", "仪表盘首页", "<1ms"},
|
||||
{"GET", "/api/items", "获取商品列表", "~1ms"},
|
||||
{"GET", "/api/items/:id", "获取商品详情", "~1ms"},
|
||||
{"GET", "/api/health", "健康检查", "<1ms"},
|
||||
}
|
||||
routeHTML := ""
|
||||
for _, r := range routes {
|
||||
color := "#00d4aa"
|
||||
routeHTML += fmt.Sprintf(`<div class="ep"><span class="mt" style="background:%s18;color:%s">%s</span><div class="ei"><div class="ep-p">%s</div><div class="ep-d">%s</div></div><div class="ep-ms">%s</div></div>`, color, color, r.method, r.path, r.desc, r.ms)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`<!DOCTYPE html>
|
||||
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>Gin Dashboard</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{--bg:#080818;--card:rgba(255,255,255,0.035);--border:rgba(255,255,255,0.07);--text:#f0f0ff;--text2:rgba(240,240,255,0.55);--text3:rgba(240,240,255,0.3);--blue:#00b4d8;--blue2:#0096c7;--radius:14px}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
||||
body::before{content:'';position:fixed;top:-40%%;left:-40%%;width:180%%;height:180%%;background:radial-gradient(ellipse at 50%% 30%%,rgba(0,180,216,0.06) 0%%,transparent 55%%);pointer-events:none}
|
||||
.c{max-width:460px;margin:0 auto;padding:0 14px 32px;position:relative;z-index:1}
|
||||
.hdr{padding:20px 2px;display:flex;justify-content:space-between;align-items:center}
|
||||
.hdr h1{font-size:22px;font-weight:800;background:linear-gradient(135deg,#00b4d8,#48cae4);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.hdr p{font-size:12px;color:var(--text2);margin-top:2px}
|
||||
.badge{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:5px 12px;border-radius:20px;background:rgba(0,180,216,0.1);color:var(--blue);border:1px solid rgba(0,180,216,0.15)}
|
||||
.badge .dot{width:7px;height:7px;border-radius:50%%;background:var(--blue);box-shadow:0 0 10px var(--blue);animation:p 2s infinite}
|
||||
@keyframes p{0%%,100%%{opacity:1}50%%{opacity:.5}}
|
||||
.row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px}
|
||||
.m{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center;backdrop-filter:blur(16px)}
|
||||
.m-val{font-size:20px;font-weight:800;letter-spacing:-0.5px;margin-bottom:2px}
|
||||
.m-label{font-size:10px;color:var(--text2);text-transform:uppercase;letter-spacing:.5px;font-weight:500}
|
||||
.m:nth-child(1) .m-val{color:#00d4aa}.m:nth-child(2) .m-val{color:var(--blue)}.m:nth-child(3) .m-val{color:#8b5cf6}
|
||||
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;margin-bottom:14px;backdrop-filter:blur(16px)}
|
||||
.card-h{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}
|
||||
.card-t{font-size:15px;font-weight:700}
|
||||
.card-b{font-size:10px;font-weight:600;padding:3px 9px;border-radius:16px;background:rgba(0,180,216,0.1);color:var(--blue)}
|
||||
.ep{display:flex;align-items:center;gap:10px;padding:11px 0;border-bottom:1px solid rgba(255,255,255,0.03)}.ep:last-child{border:none}
|
||||
.mt{font-size:10px;font-weight:800;padding:3px 8px;border-radius:6px;min-width:36px;text-align:center}
|
||||
.ei{flex:1;min-width:0}.ep-p{font-size:13px;font-weight:600;font-family:monospace}.ep-d{font-size:11px;color:var(--text2)}.ep-ms{font-size:11px;color:var(--text3);font-family:monospace}
|
||||
.chart{display:flex;align-items:flex-end;justify-content:space-between;height:130px;gap:6px}
|
||||
.bg{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;height:100%%;justify-content:flex-end}
|
||||
.b{width:100%%;max-width:28px;border-radius:5px 5px 3px 3px;background:linear-gradient(180deg,#48cae4,#0096c7);box-shadow:0 0 10px rgba(0,180,216,0.2);transform-origin:bottom;animation:grow .7s cubic-bezier(.34,1.56,.64,1) backwards}
|
||||
@keyframes grow{from{transform:scaleY(0)}to{transform:scaleY(1)}}
|
||||
.bl{font-size:9px;color:var(--text3);font-weight:500}
|
||||
.try{background:rgba(0,180,216,0.1);color:var(--blue);border:1px solid rgba(0,180,216,0.2);padding:6px 14px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer}.try:active{transform:scale(.95)}
|
||||
.result{margin-top:10px;padding:12px;background:rgba(0,0,0,0.3);border-radius:8px;font-family:monospace;font-size:11px;color:#00d4aa;max-height:180px;overflow-y:auto;display:none;white-space:pre-wrap;word-break:break-all}
|
||||
.ft{text-align:center;padding:12px}.ft-b{display:inline-flex;align-items:center;gap:6px;font-size:10px;font-weight:600;color:var(--text3);padding:5px 12px;border-radius:20px;background:rgba(0,180,216,0.04);border:1px solid rgba(0,180,216,0.08)}
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}.m,.card{animation:fadeIn .5s ease backwards}
|
||||
</style></head><body>
|
||||
<div class="c">
|
||||
<div class="hdr"><div><h1>🚀 Gin Dashboard</h1><p>高性能 Go Web 框架</p></div><div class="badge"><span class="dot"></span>运行中</div></div>
|
||||
<div class="row">
|
||||
<div class="m"><div class="m-val">%s</div><div class="m-label">运行时间</div></div>
|
||||
<div class="m"><div class="m-val">%d</div><div class="m-label">商品数</div></div>
|
||||
<div class="m"><div class="m-val">4</div><div class="m-label">路由数</div></div>
|
||||
</div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">API 路由</span><span class="card-b">⚡ RESTful</span></div>%s</div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">请求趋势</span><span class="card-b">📊 本周</span></div><div class="chart">%s</div></div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">在线测试</span><span class="card-b">🧪 交互</span></div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap"><button class="try" onclick="tryApi('/api/items')">📦 商品列表</button><button class="try" onclick="tryApi('/api/health')">💚 健康检查</button></div>
|
||||
<div class="result" id="r"></div></div>
|
||||
<div class="ft"><div class="ft-b"><span class="dot" style="width:5px;height:5px;border-radius:50%%;background:var(--blue);box-shadow:0 0 6px var(--blue)"></span>Gin · Go 1.22 · 运行中</div></div>
|
||||
</div>
|
||||
<script>function tryApi(u){var r=document.getElementById('r');r.style.display='block';r.textContent='⏳ ...';var t=performance.now();fetch(u).then(function(x){return x.json()}).then(function(d){r.textContent=JSON.stringify(d,null,2)+'\n⏱️ '+Math.round(performance.now()-t)+'ms'}).catch(function(e){r.textContent='❌ '+e.message})}</script>
|
||||
</body></html>`, uptime, len(items), routeHTML, bars)
|
||||
}
|
||||
569
app/src/main/assets/sample_projects/leaflet-bench-finder-ar/dist/index.html
vendored
Normal file
569
app/src/main/assets/sample_projects/leaflet-bench-finder-ar/dist/index.html
vendored
Normal file
@ -0,0 +1,569 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>مستكشف المقاعد</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #10b981;
|
||||
--primary-dark: #059669;
|
||||
--bg: #0f172a;
|
||||
--card: rgba(15, 23, 42, 0.92);
|
||||
--card-border: rgba(16, 185, 129, 0.15);
|
||||
--text: #f1f5f9;
|
||||
--text2: #94a3b8;
|
||||
--danger: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--radius: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a {
|
||||
background: var(--card) !important;
|
||||
color: var(--text) !important;
|
||||
border: 1px solid var(--card-border) !important;
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a:hover {
|
||||
background: var(--primary-dark) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.leaflet-control-attribution {
|
||||
background: rgba(15, 23, 42, 0.7) !important;
|
||||
color: var(--text2) !important;
|
||||
font-size: 9px !important;
|
||||
}
|
||||
|
||||
.leaflet-control-attribution a {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(180deg, rgba(15, 23, 42, 0.95) 0%, rgba(15, 23, 42, 0.7) 70%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.top-bar>* {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: linear-gradient(135deg, var(--primary), #34d399);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #10b981, #34d399);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.app-subtitle {
|
||||
font-size: 11px;
|
||||
color: var(--text2);
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--card-border);
|
||||
background: var(--card);
|
||||
animation: fadeInDown 0.5s ease;
|
||||
}
|
||||
|
||||
.status-pill .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-pill.searching .dot {
|
||||
background: var(--warning);
|
||||
}
|
||||
|
||||
.status-pill.error .dot {
|
||||
background: var(--danger);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.bottom-sheet {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
padding: 0 12px 16px;
|
||||
pointer-events: none;
|
||||
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.bottom-sheet.hidden {
|
||||
transform: translateY(calc(100% + 20px));
|
||||
}
|
||||
|
||||
.bottom-sheet>* {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: var(--card);
|
||||
backdrop-filter: blur(30px);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
box-shadow: 0 -8px 40px rgba(0, 0, 0, 0.4);
|
||||
animation: slideUp 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.info-card .handle {
|
||||
width: 36px;
|
||||
height: 4px;
|
||||
background: rgba(148, 163, 184, 0.3);
|
||||
border-radius: 2px;
|
||||
margin: 0 auto 14px;
|
||||
}
|
||||
|
||||
.bench-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.bench-icon-box {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(52, 211, 153, 0.1));
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.bench-name {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bench-detail {
|
||||
font-size: 12px;
|
||||
color: var(--text2);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
background: rgba(16, 185, 129, 0.06);
|
||||
border: 1px solid rgba(16, 185, 129, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-val {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 10px;
|
||||
color: var(--text2);
|
||||
margin-top: 2px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
background: linear-gradient(135deg, var(--primary), #34d399);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.nav-btn:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.nav-btn.secondary {
|
||||
background: rgba(148, 163, 184, 0.1);
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
box-shadow: none;
|
||||
color: var(--text2);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.locate-btn {
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
bottom: 200px;
|
||||
z-index: 1000;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: var(--card);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.locate-btn:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.locate-btn.locating {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 20px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
background: var(--bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
transition: opacity 0.5s ease, visibility 0.5s ease;
|
||||
}
|
||||
|
||||
.loading-overlay.hide {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(16, 185, 129, 0.2);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: var(--text2);
|
||||
animation: fadeInDown 0.5s ease 0.3s backwards;
|
||||
}
|
||||
|
||||
.custom-marker {
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.marker-pin {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50% 50% 50% 0;
|
||||
transform: rotate(-45deg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.marker-pin.bench {
|
||||
background: linear-gradient(135deg, var(--primary), #34d399);
|
||||
}
|
||||
|
||||
.marker-pin.user {
|
||||
background: linear-gradient(135deg, #3b82f6, #60a5fa);
|
||||
}
|
||||
|
||||
.marker-pin.nearest {
|
||||
background: linear-gradient(135deg, var(--warning), #fbbf24);
|
||||
animation: marker-bounce 0.6s ease;
|
||||
}
|
||||
|
||||
.marker-pin span {
|
||||
transform: rotate(45deg);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(40px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marker-bounce {
|
||||
0% {
|
||||
transform: rotate(-45deg) scale(0);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: rotate(-45deg) scale(1.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(-45deg) scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="loading-overlay" id="loadingOverlay">
|
||||
<div class="app-logo" style="width:64px;height:64px;font-size:32px;border-radius:18px;">🪑</div>
|
||||
<div
|
||||
style="font-size:22px;font-weight:800;background:linear-gradient(135deg,#10b981,#34d399);-webkit-background-clip:text;-webkit-text-fill-color:transparent;">
|
||||
مستكشف المقاعد</div>
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">جاري تحميل الخريطة...</div>
|
||||
</div>
|
||||
<div id="map"></div>
|
||||
<div class="top-bar">
|
||||
<div class="app-header">
|
||||
<div class="app-logo">🪑</div>
|
||||
<div>
|
||||
<div class="app-title">مستكشف المقاعد</div>
|
||||
<div class="app-subtitle">ابحث عن أقرب مقعد للراحة</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-pill searching" id="statusPill">
|
||||
<div class="dot"></div>
|
||||
<span id="statusText">جاري تحديد الموقع...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="locate-btn" id="locateBtn" onclick="locateMe()">📍</div>
|
||||
<div class="bottom-sheet hidden" id="bottomSheet">
|
||||
<div class="info-card">
|
||||
<div class="handle"></div>
|
||||
<div class="bench-info">
|
||||
<div class="bench-icon-box">🪑</div>
|
||||
<div>
|
||||
<div class="bench-name" id="benchName">أقرب مقعد</div>
|
||||
<div class="bench-detail" id="benchDetail">جاري البحث...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-row">
|
||||
<div class="stat-item">
|
||||
<div class="stat-val" id="statDist">--</div>
|
||||
<div class="stat-label">المسافة</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-val" id="statTime">--</div>
|
||||
<div class="stat-label">المشي</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-val" id="statTotal">--</div>
|
||||
<div class="stat-label">القريبة</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="nav-btn" id="navBtn" onclick="navigateToNearest()">🧭 بدء الملاحة</button>
|
||||
<button class="nav-btn secondary" onclick="searchNearby()">🔄 بحث مرة أخرى</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let map, userMarker, routeLine, benchMarkers = [];
|
||||
let userLat, userLng, nearestBench = null, allBenches = [];
|
||||
function initMap() {
|
||||
map = L.map('map', { center: [48.8566, 2.3522], zoom: 15, zoomControl: false, attributionControl: true });
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>', maxZoom: 19 }).addTo(map);
|
||||
L.control.zoom({ position: 'topleft' }).addTo(map);
|
||||
setTimeout(() => { document.getElementById('loadingOverlay').classList.add('hide'); }, 1000);
|
||||
locateMe();
|
||||
}
|
||||
function locateMe() {
|
||||
const btn = document.getElementById('locateBtn'); btn.classList.add('locating');
|
||||
updateStatus('searching', 'جاري تحديد الموقع...');
|
||||
if (!navigator.geolocation) { updateStatus('error', 'تحديد الموقع غير مدعوم'); btn.classList.remove('locating'); onLocationFound(48.8584, 2.2945); return; }
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => { btn.classList.remove('locating'); onLocationFound(pos.coords.latitude, pos.coords.longitude); },
|
||||
() => { btn.classList.remove('locating'); updateStatus('error', 'فشل تحديد الموقع'); onLocationFound(48.8584, 2.2945); },
|
||||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }
|
||||
);
|
||||
}
|
||||
function onLocationFound(lat, lng) {
|
||||
userLat = lat; userLng = lng; map.setView([lat, lng], 16);
|
||||
if (userMarker) map.removeLayer(userMarker);
|
||||
userMarker = L.marker([lat, lng], { icon: L.divIcon({ className: 'custom-marker', html: '<div class="marker-pin user"><span>📱</span></div>', iconSize: [36, 36], iconAnchor: [18, 36] }) }).addTo(map).bindPopup('<b>📱 موقعي</b>');
|
||||
L.circle([lat, lng], { radius: 50, color: '#3b82f6', fillColor: '#3b82f6', fillOpacity: 0.08, weight: 1, opacity: 0.3 }).addTo(map);
|
||||
searchNearby();
|
||||
}
|
||||
function searchNearby() {
|
||||
if (!userLat || !userLng) { locateMe(); return; }
|
||||
updateStatus('searching', 'جاري البحث عن مقاعد...');
|
||||
document.getElementById('bottomSheet').classList.add('hidden');
|
||||
benchMarkers.forEach(m => map.removeLayer(m)); benchMarkers = [];
|
||||
if (routeLine) map.removeLayer(routeLine);
|
||||
const query = `[out:json][timeout:15];(node["amenity"="bench"](around:500,${userLat},${userLng});node["leisure"="picnic_table"](around:500,${userLat},${userLng}););out body;`;
|
||||
fetch('https://overpass-api.de/api/interpreter?data=' + encodeURIComponent(query))
|
||||
.then(r => r.json()).then(d => { allBenches = d.elements || []; if (!allBenches.length) allBenches = genBenches(userLat, userLng); displayBenches(allBenches); })
|
||||
.catch(() => { allBenches = genBenches(userLat, userLng); displayBenches(allBenches); });
|
||||
}
|
||||
function genBenches(lat, lng) {
|
||||
const n = ['مقعد الحديقة', 'مقعد الشارع', 'مقعد الحديقة العامة', 'مقعد البحيرة', 'مقعد الساحة', 'مقعد الظل', 'مقعد المسار', 'مقعد المنظر', 'مقعد المجتمع', 'مقعد النهر'];
|
||||
const b = []; for (let i = 0; i < 10 + Math.floor(Math.random() * 6); i++) {
|
||||
const a = Math.random() * Math.PI * 2, d = 50 + Math.random() * 450;
|
||||
b.push({ lat: lat + (d / 111320) * Math.cos(a), lon: lng + (d / (111320 * Math.cos(lat * Math.PI / 180))) * Math.sin(a), tags: { name: n[i % n.length], amenity: 'bench', material: ['خشب', 'معدن', 'حجر', 'خرسانة'][Math.floor(Math.random() * 4)], backrest: Math.random() > 0.3 ? 'yes' : 'no' } });
|
||||
} return b;
|
||||
}
|
||||
function displayBenches(benches) {
|
||||
if (!benches.length) { updateStatus('error', 'لم يتم العثور على مقاعد'); return; }
|
||||
let nearest = null, minDist = Infinity;
|
||||
benches.forEach((b, i) => {
|
||||
const dist = getDist(userLat, userLng, b.lat, b.lon);
|
||||
if (dist < minDist) { minDist = dist; nearest = { ...b, distance: dist, index: i }; }
|
||||
benchMarkers.push(L.marker([b.lat, b.lon], { icon: L.divIcon({ className: 'custom-marker', html: '<div class="marker-pin bench"><span>🪑</span></div>', iconSize: [36, 36], iconAnchor: [18, 36] }) }).addTo(map).bindPopup('<b>🪑 ' + (b.tags?.name || 'مقعد') + '</b><br>📏 ' + fmtDist(dist)));
|
||||
});
|
||||
if (nearest) {
|
||||
nearestBench = nearest;
|
||||
if (benchMarkers[nearest.index]) map.removeLayer(benchMarkers[nearest.index]);
|
||||
benchMarkers[nearest.index] = L.marker([nearest.lat, nearest.lon], { icon: L.divIcon({ className: 'custom-marker', html: '<div class="marker-pin nearest"><span>⭐</span></div>', iconSize: [40, 40], iconAnchor: [20, 40] }) }).addTo(map).bindPopup('<b>⭐ أقرب مقعد</b><br>' + fmtDist(nearest.distance)).openPopup();
|
||||
drawRoute(userLat, userLng, nearest.lat, nearest.lon);
|
||||
document.getElementById('benchName').textContent = nearest.tags?.name || 'مقعد عام';
|
||||
document.getElementById('benchDetail').textContent = 'المادة: ' + (nearest.tags?.material || 'غير معروف');
|
||||
document.getElementById('statDist').textContent = fmtDist(nearest.distance);
|
||||
document.getElementById('statTime').textContent = fmtTime(nearest.distance);
|
||||
document.getElementById('statTotal').textContent = benches.length;
|
||||
document.getElementById('bottomSheet').classList.remove('hidden');
|
||||
updateStatus('', 'تم العثور على ' + benches.length + ' مقعد');
|
||||
map.fitBounds(L.latLngBounds([userLat, userLng], [nearest.lat, nearest.lon]).pad(0.3));
|
||||
}
|
||||
}
|
||||
function drawRoute(fLat, fLng, tLat, tLng) {
|
||||
if (routeLine) map.removeLayer(routeLine);
|
||||
fetch(`https://router.project-osrm.org/route/v1/foot/${fLng},${fLat};${tLng},${tLat}?overview=full&geometries=geojson`)
|
||||
.then(r => r.json()).then(d => {
|
||||
if (d.routes?.length) {
|
||||
const c = d.routes[0].geometry.coordinates.map(p => [p[1], p[0]]);
|
||||
L.polyline(c, { color: '#064e3b', weight: 8, opacity: 0.4, lineCap: 'round' }).addTo(map);
|
||||
routeLine = L.polyline(c, { color: '#10b981', weight: 5, opacity: 0.9, lineCap: 'round', dashArray: '12, 8' }).addTo(map);
|
||||
if (d.routes[0].distance) document.getElementById('statDist').textContent = fmtDist(d.routes[0].distance);
|
||||
if (d.routes[0].duration) document.getElementById('statTime').textContent = Math.ceil(d.routes[0].duration / 60) + 'د';
|
||||
}
|
||||
}).catch(() => { routeLine = L.polyline([[fLat, fLng], [tLat, tLng]], { color: '#10b981', weight: 4, opacity: 0.7, dashArray: '10,10' }).addTo(map); });
|
||||
}
|
||||
function navigateToNearest() { if (nearestBench) { map.setView([nearestBench.lat, nearestBench.lon], 18); benchMarkers[nearestBench.index]?.openPopup(); } }
|
||||
function getDist(a, b, c, d) { const R = 6371000, dL = (c - a) * Math.PI / 180, dG = (d - b) * Math.PI / 180, x = Math.sin(dL / 2) ** 2 + Math.cos(a * Math.PI / 180) * Math.cos(c * Math.PI / 180) * Math.sin(dG / 2) ** 2; return R * 2 * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x)); }
|
||||
function fmtDist(m) { return m < 1000 ? Math.round(m) + 'م' : (m / 1000).toFixed(1) + 'كم'; }
|
||||
function fmtTime(m) { const min = Math.ceil(m / 80); return min < 1 ? '<1د' : min + 'د'; }
|
||||
function updateStatus(t, x) { document.getElementById('statusPill').className = 'status-pill' + (t ? ' ' + t : ''); document.getElementById('statusText').textContent = x; }
|
||||
initMap();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
639
app/src/main/assets/sample_projects/leaflet-bench-finder-en/dist/index.html
vendored
Normal file
639
app/src/main/assets/sample_projects/leaflet-bench-finder-en/dist/index.html
vendored
Normal file
@ -0,0 +1,639 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Bench Explorer</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #10b981;
|
||||
--primary-dark: #059669;
|
||||
--bg: #0f172a;
|
||||
--card: rgba(15, 23, 42, 0.92);
|
||||
--card-border: rgba(16, 185, 129, 0.15);
|
||||
--text: #f1f5f9;
|
||||
--text2: #94a3b8;
|
||||
--danger: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--radius: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a {
|
||||
background: var(--card) !important;
|
||||
color: var(--text) !important;
|
||||
border: 1px solid var(--card-border) !important;
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a:hover {
|
||||
background: var(--primary-dark) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.leaflet-control-attribution {
|
||||
background: rgba(15, 23, 42, 0.7) !important;
|
||||
color: var(--text2) !important;
|
||||
font-size: 9px !important;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.leaflet-control-attribution a {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(180deg, rgba(15, 23, 42, 0.95) 0%, rgba(15, 23, 42, 0.7) 70%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.top-bar>* {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: linear-gradient(135deg, var(--primary), #34d399);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #10b981, #34d399);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.app-subtitle {
|
||||
font-size: 11px;
|
||||
color: var(--text2);
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--card-border);
|
||||
background: var(--card);
|
||||
animation: fadeInDown 0.5s ease;
|
||||
}
|
||||
|
||||
.status-pill .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-pill.searching .dot {
|
||||
background: var(--warning);
|
||||
}
|
||||
|
||||
.status-pill.error .dot {
|
||||
background: var(--danger);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.bottom-sheet {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
padding: 0 12px 16px;
|
||||
pointer-events: none;
|
||||
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.bottom-sheet.hidden {
|
||||
transform: translateY(calc(100% + 20px));
|
||||
}
|
||||
|
||||
.bottom-sheet>* {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: var(--card);
|
||||
backdrop-filter: blur(30px);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
box-shadow: 0 -8px 40px rgba(0, 0, 0, 0.4);
|
||||
animation: slideUp 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.info-card .handle {
|
||||
width: 36px;
|
||||
height: 4px;
|
||||
background: rgba(148, 163, 184, 0.3);
|
||||
border-radius: 2px;
|
||||
margin: 0 auto 14px;
|
||||
}
|
||||
|
||||
.bench-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.bench-icon-box {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(52, 211, 153, 0.1));
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.bench-name {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bench-detail {
|
||||
font-size: 12px;
|
||||
color: var(--text2);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
background: rgba(16, 185, 129, 0.06);
|
||||
border: 1px solid rgba(16, 185, 129, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-val {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 10px;
|
||||
color: var(--text2);
|
||||
margin-top: 2px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
background: linear-gradient(135deg, var(--primary), #34d399);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.nav-btn:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.nav-btn.secondary {
|
||||
background: rgba(148, 163, 184, 0.1);
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
box-shadow: none;
|
||||
color: var(--text2);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.locate-btn {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: 200px;
|
||||
z-index: 1000;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: var(--card);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.locate-btn:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.locate-btn.locating {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 20px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
background: var(--bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
transition: opacity 0.5s ease, visibility 0.5s ease;
|
||||
}
|
||||
|
||||
.loading-overlay.hide {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(16, 185, 129, 0.2);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: var(--text2);
|
||||
animation: fadeInDown 0.5s ease 0.3s backwards;
|
||||
}
|
||||
|
||||
.custom-marker {
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.marker-pin {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50% 50% 50% 0;
|
||||
transform: rotate(-45deg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.marker-pin.bench {
|
||||
background: linear-gradient(135deg, var(--primary), #34d399);
|
||||
}
|
||||
|
||||
.marker-pin.user {
|
||||
background: linear-gradient(135deg, #3b82f6, #60a5fa);
|
||||
}
|
||||
|
||||
.marker-pin.nearest {
|
||||
background: linear-gradient(135deg, var(--warning), #fbbf24);
|
||||
animation: marker-bounce 0.6s ease;
|
||||
}
|
||||
|
||||
.marker-pin span {
|
||||
transform: rotate(45deg);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(40px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marker-bounce {
|
||||
0% {
|
||||
transform: rotate(-45deg) scale(0);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: rotate(-45deg) scale(1.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(-45deg) scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="loading-overlay" id="loadingOverlay">
|
||||
<div class="app-logo" style="width:64px;height:64px;font-size:32px;border-radius:18px;">🪑</div>
|
||||
<div
|
||||
style="font-size:22px;font-weight:800;background:linear-gradient(135deg,#10b981,#34d399);-webkit-background-clip:text;-webkit-text-fill-color:transparent;">
|
||||
Bench Explorer</div>
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">Loading map...</div>
|
||||
</div>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<div class="top-bar">
|
||||
<div class="app-header">
|
||||
<div class="app-logo">🪑</div>
|
||||
<div>
|
||||
<div class="app-title">Bench Explorer</div>
|
||||
<div class="app-subtitle">Find the nearest bench to rest</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-pill searching" id="statusPill">
|
||||
<div class="dot"></div>
|
||||
<span id="statusText">Getting location...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="locate-btn" id="locateBtn" onclick="locateMe()">📍</div>
|
||||
|
||||
<div class="bottom-sheet hidden" id="bottomSheet">
|
||||
<div class="info-card">
|
||||
<div class="handle"></div>
|
||||
<div class="bench-info">
|
||||
<div class="bench-icon-box">🪑</div>
|
||||
<div>
|
||||
<div class="bench-name" id="benchName">Nearest Bench</div>
|
||||
<div class="bench-detail" id="benchDetail">Searching...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-row">
|
||||
<div class="stat-item">
|
||||
<div class="stat-val" id="statDist">--</div>
|
||||
<div class="stat-label">Distance</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-val" id="statTime">--</div>
|
||||
<div class="stat-label">Walk</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-val" id="statTotal">--</div>
|
||||
<div class="stat-label">Nearby</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="nav-btn" id="navBtn" onclick="navigateToNearest()">
|
||||
🧭 Start Navigation
|
||||
</button>
|
||||
<button class="nav-btn secondary" onclick="searchNearby()">
|
||||
🔄 Search Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let map, userMarker, routeLine, benchMarkers = [];
|
||||
let userLat, userLng;
|
||||
let nearestBench = null;
|
||||
let allBenches = [];
|
||||
|
||||
function initMap() {
|
||||
map = L.map('map', {
|
||||
center: [48.8566, 2.3522],
|
||||
zoom: 15,
|
||||
zoomControl: false,
|
||||
attributionControl: true
|
||||
});
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
L.control.zoom({ position: 'topright' }).addTo(map);
|
||||
setTimeout(() => { document.getElementById('loadingOverlay').classList.add('hide'); }, 1000);
|
||||
locateMe();
|
||||
}
|
||||
|
||||
function locateMe() {
|
||||
const btn = document.getElementById('locateBtn');
|
||||
btn.classList.add('locating');
|
||||
updateStatus('searching', 'Getting location...');
|
||||
if (!navigator.geolocation) {
|
||||
updateStatus('error', 'Geolocation not supported');
|
||||
btn.classList.remove('locating');
|
||||
onLocationFound(48.8584, 2.2945);
|
||||
return;
|
||||
}
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => { btn.classList.remove('locating'); onLocationFound(pos.coords.latitude, pos.coords.longitude); },
|
||||
(err) => { btn.classList.remove('locating'); updateStatus('error', 'Location failed, using default'); onLocationFound(48.8584, 2.2945); },
|
||||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }
|
||||
);
|
||||
}
|
||||
|
||||
function onLocationFound(lat, lng) {
|
||||
userLat = lat; userLng = lng;
|
||||
map.setView([lat, lng], 16);
|
||||
if (userMarker) map.removeLayer(userMarker);
|
||||
const userIcon = L.divIcon({ className: 'custom-marker', html: '<div class="marker-pin user"><span>📱</span></div>', iconSize: [36, 36], iconAnchor: [18, 36] });
|
||||
userMarker = L.marker([lat, lng], { icon: userIcon }).addTo(map).bindPopup('<b>📱 My Location</b><br>Lat: ' + lat.toFixed(6) + '<br>Lng: ' + lng.toFixed(6));
|
||||
L.circle([lat, lng], { radius: 50, color: '#3b82f6', fillColor: '#3b82f6', fillOpacity: 0.08, weight: 1, opacity: 0.3 }).addTo(map);
|
||||
searchNearby();
|
||||
}
|
||||
|
||||
function searchNearby() {
|
||||
if (!userLat || !userLng) { locateMe(); return; }
|
||||
updateStatus('searching', 'Searching for benches...');
|
||||
document.getElementById('bottomSheet').classList.add('hidden');
|
||||
benchMarkers.forEach(m => map.removeLayer(m));
|
||||
benchMarkers = [];
|
||||
if (routeLine) map.removeLayer(routeLine);
|
||||
const query = `[out:json][timeout:15];(node["amenity"="bench"](around:500,${userLat},${userLng});node["leisure"="picnic_table"](around:500,${userLat},${userLng}););out body;`;
|
||||
fetch('https://overpass-api.de/api/interpreter?data=' + encodeURIComponent(query))
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
allBenches = data.elements || [];
|
||||
if (allBenches.length === 0) { allBenches = generateSimulatedBenches(userLat, userLng); updateStatus('', 'Using simulated bench data'); }
|
||||
displayBenches(allBenches);
|
||||
})
|
||||
.catch(err => { allBenches = generateSimulatedBenches(userLat, userLng); displayBenches(allBenches); });
|
||||
}
|
||||
|
||||
function generateSimulatedBenches(lat, lng) {
|
||||
const names = ['Park Bench', 'Street Seat', 'Garden Bench', 'Lakeside Seat', 'Plaza Bench', 'Shaded Bench', 'Trail Rest', 'Viewpoint Seat', 'Community Bench', 'Riverside Bench', 'Campus Bench', 'Mall Bench'];
|
||||
const benches = [];
|
||||
const count = 8 + Math.floor(Math.random() * 8);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const dist = 50 + Math.random() * 450;
|
||||
const dLat = (dist / 111320) * Math.cos(angle);
|
||||
const dLng = (dist / (111320 * Math.cos(lat * Math.PI / 180))) * Math.sin(angle);
|
||||
benches.push({ lat: lat + dLat, lon: lng + dLng, tags: { name: names[i % names.length], amenity: 'bench', material: ['wood', 'metal', 'stone', 'concrete'][Math.floor(Math.random() * 4)], backrest: Math.random() > 0.3 ? 'yes' : 'no', seats: (2 + Math.floor(Math.random() * 4)).toString() } });
|
||||
}
|
||||
return benches;
|
||||
}
|
||||
|
||||
function displayBenches(benches) {
|
||||
if (benches.length === 0) { updateStatus('error', 'No benches found nearby'); return; }
|
||||
let nearest = null, minDist = Infinity;
|
||||
benches.forEach((bench, idx) => {
|
||||
const bLat = bench.lat, bLng = bench.lon;
|
||||
const dist = getDistance(userLat, userLng, bLat, bLng);
|
||||
if (dist < minDist) { minDist = dist; nearest = { ...bench, distance: dist, index: idx }; }
|
||||
const benchIcon = L.divIcon({ className: 'custom-marker', html: '<div class="marker-pin bench"><span>🪑</span></div>', iconSize: [36, 36], iconAnchor: [18, 36] });
|
||||
const name = bench.tags?.name || 'Public Bench';
|
||||
const material = bench.tags?.material || 'Unknown';
|
||||
const backrest = bench.tags?.backrest === 'yes' ? 'Yes' : 'No';
|
||||
const marker = L.marker([bLat, bLng], { icon: benchIcon }).addTo(map).bindPopup('<b>🪑 ' + name + '</b><br>📏 Distance: ' + formatDistance(dist) + '<br>🪵 Material: ' + material + '<br>🔙 Backrest: ' + backrest);
|
||||
benchMarkers.push(marker);
|
||||
});
|
||||
if (nearest) {
|
||||
nearestBench = nearest;
|
||||
const nearestIcon = L.divIcon({ className: 'custom-marker', html: '<div class="marker-pin nearest"><span>⭐</span></div>', iconSize: [40, 40], iconAnchor: [20, 40] });
|
||||
if (benchMarkers[nearest.index]) map.removeLayer(benchMarkers[nearest.index]);
|
||||
const nearestMarker = L.marker([nearest.lat, nearest.lon], { icon: nearestIcon }).addTo(map).bindPopup('<b>⭐ Nearest Bench</b><br>Distance: ' + formatDistance(nearest.distance)).openPopup();
|
||||
benchMarkers[nearest.index] = nearestMarker;
|
||||
drawRoute(userLat, userLng, nearest.lat, nearest.lon);
|
||||
document.getElementById('benchName').textContent = nearest.tags?.name || 'Public Bench';
|
||||
document.getElementById('benchDetail').textContent = 'Material: ' + (nearest.tags?.material || 'Unknown') + ' · Backrest: ' + (nearest.tags?.backrest === 'yes' ? 'Yes' : 'No');
|
||||
document.getElementById('statDist').textContent = formatDistance(nearest.distance);
|
||||
document.getElementById('statTime').textContent = formatWalkTime(nearest.distance);
|
||||
document.getElementById('statTotal').textContent = benches.length;
|
||||
document.getElementById('bottomSheet').classList.remove('hidden');
|
||||
updateStatus('', 'Found ' + benches.length + ' benches');
|
||||
map.fitBounds(L.latLngBounds([userLat, userLng], [nearest.lat, nearest.lon]).pad(0.3));
|
||||
}
|
||||
}
|
||||
|
||||
function drawRoute(fromLat, fromLng, toLat, toLng) {
|
||||
if (routeLine) map.removeLayer(routeLine);
|
||||
fetch(`https://router.project-osrm.org/route/v1/foot/${fromLng},${fromLat};${toLng},${toLat}?overview=full&geometries=geojson`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.routes && data.routes.length > 0) {
|
||||
const coords = data.routes[0].geometry.coordinates.map(c => [c[1], c[0]]);
|
||||
L.polyline(coords, { color: '#064e3b', weight: 8, opacity: 0.4, lineCap: 'round', lineJoin: 'round' }).addTo(map);
|
||||
routeLine = L.polyline(coords, { color: '#10b981', weight: 5, opacity: 0.9, lineCap: 'round', lineJoin: 'round', dashArray: '12, 8' }).addTo(map);
|
||||
const route = data.routes[0];
|
||||
if (route.distance && route.duration) {
|
||||
document.getElementById('statDist').textContent = formatDistance(route.distance);
|
||||
document.getElementById('statTime').textContent = Math.ceil(route.duration / 60) + 'min';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => { routeLine = L.polyline([[fromLat, fromLng], [toLat, toLng]], { color: '#10b981', weight: 4, opacity: 0.7, dashArray: '10, 10' }).addTo(map); });
|
||||
}
|
||||
|
||||
function navigateToNearest() {
|
||||
if (!nearestBench) return;
|
||||
map.setView([nearestBench.lat, nearestBench.lon], 18);
|
||||
benchMarkers[nearestBench.index]?.openPopup();
|
||||
}
|
||||
|
||||
function getDistance(lat1, lng1, lat2, lng2) {
|
||||
const R = 6371000, dLat = (lat2 - lat1) * Math.PI / 180, dLng = (lng2 - lng1) * Math.PI / 180;
|
||||
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
||||
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
}
|
||||
function formatDistance(m) { return m < 1000 ? Math.round(m) + 'm' : (m / 1000).toFixed(1) + 'km'; }
|
||||
function formatWalkTime(m) { const min = Math.ceil(m / 80); return min < 1 ? '<1min' : min >= 60 ? Math.floor(min / 60) + 'h' + (min % 60) + 'min' : min + 'min'; }
|
||||
function updateStatus(type, text) { const p = document.getElementById('statusPill'); p.className = 'status-pill' + (type ? ' ' + type : ''); document.getElementById('statusText').textContent = text; }
|
||||
initMap();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "todo-app",
|
||||
"version": "1.0.0",
|
||||
"main": "server.js",
|
||||
"scripts": { "start": "node server.js" },
|
||||
"dependencies": {}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
|
||||
|
||||
|
||||
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const DATA_FILE = path.join(__dirname, 'todos.json');
|
||||
const loadTodos = () => { try { if (fs.existsSync(DATA_FILE)) return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); } catch (e) { } return [{ id: 1, text: 'مرحباً بتطبيق المهام 👋', done: false, createdAt: Date.now() }, { id: 2, text: 'اضغط الدائرة لإكمال المهمة ✓', done: true, createdAt: Date.now() - 1000 }, { id: 3, text: 'اضغط × لحذف المهمة', done: false, createdAt: Date.now() - 2000 }]; };
|
||||
const saveTodos = t => fs.writeFileSync(DATA_FILE, JSON.stringify(t, null, 2));
|
||||
let todos = loadTodos(), nextId = Math.max(...todos.map(t => t.id), 0) + 1;
|
||||
function parseBody(req) { return new Promise(r => { let b = ''; req.on('data', c => b += c); req.on('end', () => { try { r(JSON.parse(b)) } catch (e) { r({}) } }) }) }
|
||||
function sendJson(res, data, s = 200) { res.writeHead(s, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(data)) }
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
if (m === 'GET' && p === '/') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); return res.end(getPage()) }
|
||||
if (m === 'GET' && p === '/api/todos') return sendJson(res, { success: true, data: todos });
|
||||
if (m === 'POST' && p === '/api/todos') { const { text } = await parseBody(req); if (!text?.trim()) return sendJson(res, { success: false }, 400); const t = { id: nextId++, text: text.trim(), done: false, createdAt: Date.now() }; todos.unshift(t); saveTodos(todos); return sendJson(res, { success: true, data: t }) }
|
||||
const tog = p.match(/^\/api\/todos\/(\d+)\/toggle$/); if (m === 'PATCH' && tog) { const t = todos.find(x => x.id === parseInt(tog[1])); if (!t) return sendJson(res, { success: false }, 404); t.done = !t.done; saveTodos(todos); return sendJson(res, { success: true, data: t }) }
|
||||
if (m === 'DELETE' && p === '/api/todos/completed/all') { todos = todos.filter(t => !t.done); saveTodos(todos); return sendJson(res, { success: true }) }
|
||||
const del = p.match(/^\/api\/todos\/(\d+)$/); if (m === 'DELETE' && del) { const i = todos.findIndex(t => t.id === parseInt(del[1])); if (i === -1) return sendJson(res, { success: false }, 404); todos.splice(i, 1); saveTodos(todos); return sendJson(res, { success: true }) }
|
||||
sendJson(res, { error: 'Not Found' }, 404);
|
||||
});
|
||||
server.listen(PORT, '0.0.0.0', () => console.log('Todo on http://0.0.0.0:' + PORT));
|
||||
|
||||
function getPage() {
|
||||
return `<!DOCTYPE html><html lang="ar" dir="rtl"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>مهام</title>
|
||||
<style>*{margin:0;padding:0;box-sizing:border-box}:root{--bg:#0a0a1a;--card:rgba(255,255,255,0.04);--border:rgba(255,255,255,0.07);--text:#f0f0ff;--text2:rgba(240,240,255,0.55);--text3:rgba(240,240,255,0.3);--purple:#a855f7;--radius:14px}body{font-family:-apple-system,BlinkMacSystemFont,Tahoma,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;direction:rtl}body::before{content:'';position:fixed;top:-40%;left:-40%;width:180%;height:180%;background:radial-gradient(ellipse at 50% 30%,rgba(168,85,247,0.06) 0%,transparent 55%);pointer-events:none}.c{max-width:460px;margin:0 auto;padding:0 14px 32px;position:relative;z-index:1}.hdr{text-align:center;padding:28px 0 20px}.hdr h1{font-size:24px;font-weight:800;background:linear-gradient(135deg,#a855f7,#ec4899);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.hdr p{font-size:12px;color:var(--text2);margin-top:4px}.stats{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px}.stat{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center}.stat-val{font-size:22px;font-weight:800;color:var(--purple)}.stat-label{font-size:10px;color:var(--text2);margin-top:2px}.input-row{display:flex;gap:8px;margin-bottom:16px}.input-row input{flex:1;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:12px 14px;color:var(--text);font-size:14px;outline:none}.input-row input:focus{border-color:var(--purple)}.input-row input::placeholder{color:var(--text3)}.add-btn{background:linear-gradient(135deg,#a855f7,#ec4899);color:#fff;border:none;border-radius:10px;padding:0 18px;font-size:18px;font-weight:800;cursor:pointer}.filter-row{display:flex;gap:6px;margin-bottom:14px}.filter{flex:1;padding:8px;border-radius:8px;background:var(--card);border:1px solid var(--border);color:var(--text2);font-size:12px;font-weight:600;text-align:center;cursor:pointer}.filter.active{background:rgba(168,85,247,0.1);color:var(--purple)}.todo-list{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}.todo{display:flex;align-items:center;gap:12px;padding:14px 16px;border-bottom:1px solid rgba(255,255,255,0.03);cursor:pointer}.todo:last-child{border:none}.todo.done .t-text{text-decoration:line-through;opacity:.4}.check{width:22px;height:22px;border-radius:50%;border:2px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:12px;flex-shrink:0}.todo.done .check{background:var(--purple);border-color:var(--purple);color:#fff}.t-text{flex:1;font-size:14px;font-weight:500}.del{width:28px;height:28px;border-radius:8px;background:rgba(239,68,68,0.1);border:none;color:#ef4444;font-size:13px;cursor:pointer;display:flex;align-items:center;justify-content:center}.empty{text-align:center;padding:32px;color:var(--text3)}.ft{text-align:center;padding:16px}.ft-b{font-size:10px;font-weight:600;color:var(--text3);padding:5px 12px;border-radius:20px;background:rgba(168,85,247,0.04)}.clr{background:rgba(168,85,247,0.1);color:var(--purple);border:1px solid rgba(168,85,247,0.2);padding:8px 16px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;margin-top:12px}.clr:disabled{opacity:.3}@keyframes fadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.stat,.todo{animation:fadeIn .4s ease backwards}</style></head><body>
|
||||
<div class="c"><div class="hdr"><h1>📝 قائمة المهام</h1><p>Express + Node.js</p></div>
|
||||
<div class="stats"><div class="stat"><div class="stat-val" id="total">0</div><div class="stat-label">الكل</div></div><div class="stat"><div class="stat-val" id="active">0</div><div class="stat-label">نشط</div></div><div class="stat"><div class="stat-val" id="done">0</div><div class="stat-label">مكتمل</div></div></div>
|
||||
<div class="input-row"><input type="text" id="inp" placeholder="أضف مهمة..." maxlength="100" onkeydown="if(event.key==='Enter')addTodo()"><button class="add-btn" onclick="addTodo()">+</button></div>
|
||||
<div class="filter-row"><div class="filter active" onclick="setFilter('all',this)">الكل</div><div class="filter" onclick="setFilter('active',this)">نشط</div><div class="filter" onclick="setFilter('done',this)">مكتمل</div></div>
|
||||
<div class="todo-list" id="list"></div>
|
||||
<div style="text-align:center"><button class="clr" id="clrBtn" onclick="clearDone()" disabled>حذف المكتمل</button></div>
|
||||
<div class="ft"><div class="ft-b">📝 Express · Node.js · WebToApp</div></div></div>
|
||||
<script>var todos=[],filter='all';function load(){fetch('/api/todos').then(function(r){return r.json()}).then(function(j){todos=j.data||[];render()})}function render(){var f=todos.filter(function(t){return filter==='all'?true:filter==='active'?!t.done:t.done});var list=document.getElementById('list');document.getElementById('total').textContent=todos.length;document.getElementById('active').textContent=todos.filter(function(t){return!t.done}).length;var dn=todos.filter(function(t){return t.done}).length;document.getElementById('done').textContent=dn;document.getElementById('clrBtn').disabled=dn===0;if(f.length===0){list.innerHTML='<div class="empty">🎉 لا توجد مهام</div>';return}list.innerHTML=f.map(function(t,i){return'<div class="todo'+(t.done?' done':'')+'" style="animation-delay:'+(i*.04)+'s"><div class="check" onclick="tog('+t.id+')">'+(t.done?'✓':'')+'</div><div class="t-text" onclick="tog('+t.id+')">'+esc(t.text)+'</div><button class="del" onclick="del('+t.id+')">×</button></div>'}).join('')}function esc(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML}function addTodo(){var v=document.getElementById('inp').value.trim();if(!v)return;fetch('/api/todos',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:v})}).then(function(r){return r.json()}).then(function(j){if(j.success){todos.unshift(j.data);document.getElementById('inp').value='';render()}})}function tog(id){fetch('/api/todos/'+id+'/toggle',{method:'PATCH'}).then(function(r){return r.json()}).then(function(j){if(j.success){var t=todos.find(function(x){return x.id===id});if(t)t.done=j.data.done;render()}})}function del(id){fetch('/api/todos/'+id,{method:'DELETE'}).then(function(r){if(r.ok){todos=todos.filter(function(t){return t.id!==id});render()}})}function clearDone(){fetch('/api/todos/completed/all',{method:'DELETE'}).then(function(r){if(r.ok){todos=todos.filter(function(t){return!t.done});render()}})}function setFilter(f,el){filter=f;document.querySelectorAll('.filter').forEach(function(e){e.classList.remove('active')});el.classList.add('active');render()}load();</script></body></html>`;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "todo-app",
|
||||
"version": "1.0.0",
|
||||
"description": "Todo App - Zero dependency task management with Material Design UI",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
|
||||
|
||||
|
||||
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const DATA_FILE = path.join(__dirname, 'todos.json');
|
||||
|
||||
const loadTodos = () => {
|
||||
try { if (fs.existsSync(DATA_FILE)) return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); } catch (e) { }
|
||||
return [
|
||||
{ id: 1, text: 'Welcome to Todo App 👋', done: false, createdAt: Date.now() },
|
||||
{ id: 2, text: 'Click circle to complete ✓', done: true, createdAt: Date.now() - 1000 },
|
||||
{ id: 3, text: 'Click × to delete task', done: false, createdAt: Date.now() - 2000 },
|
||||
{ id: 4, text: 'Data persists locally', done: false, createdAt: Date.now() - 3000 }
|
||||
];
|
||||
};
|
||||
const saveTodos = (t) => fs.writeFileSync(DATA_FILE, JSON.stringify(t, null, 2));
|
||||
let todos = loadTodos(); let nextId = Math.max(...todos.map(t => t.id), 0) + 1;
|
||||
|
||||
function parseBody(req) { return new Promise(r => { let b = ''; req.on('data', c => b += c); req.on('end', () => { try { r(JSON.parse(b)) } catch (e) { r({}) } }); }); }
|
||||
function sendJson(res, data, s = 200) { res.writeHead(s, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(data)); }
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
const p = url.pathname, m = req.method;
|
||||
if (m === 'GET' && p === '/') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); return res.end(getPage()); }
|
||||
if (m === 'GET' && p === '/api/todos') return sendJson(res, { success: true, data: todos });
|
||||
if (m === 'POST' && p === '/api/todos') { const { text } = await parseBody(req); if (!text?.trim()) return sendJson(res, { success: false }, 400); const t = { id: nextId++, text: text.trim(), done: false, createdAt: Date.now() }; todos.unshift(t); saveTodos(todos); return sendJson(res, { success: true, data: t }); }
|
||||
const tog = p.match(/^\/api\/todos\/(\d+)\/toggle$/); if (m === 'PATCH' && tog) { const t = todos.find(x => x.id === parseInt(tog[1])); if (!t) return sendJson(res, { success: false }, 404); t.done = !t.done; saveTodos(todos); return sendJson(res, { success: true, data: t }) }
|
||||
if (m === 'DELETE' && p === '/api/todos/completed/all') { todos = todos.filter(t => !t.done); saveTodos(todos); return sendJson(res, { success: true }) }
|
||||
const del = p.match(/^\/api\/todos\/(\d+)$/); if (m === 'DELETE' && del) { const i = todos.findIndex(t => t.id === parseInt(del[1])); if (i === -1) return sendJson(res, { success: false }, 404); todos.splice(i, 1); saveTodos(todos); return sendJson(res, { success: true }) }
|
||||
sendJson(res, { error: 'Not Found' }, 404);
|
||||
});
|
||||
server.listen(PORT, '0.0.0.0', () => console.log('Todo on http://0.0.0.0:' + PORT));
|
||||
|
||||
function getPage() {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>Todo</title>
|
||||
<style>*{margin:0;padding:0;box-sizing:border-box}:root{--bg:#0a0a1a;--card:rgba(255,255,255,0.04);--border:rgba(255,255,255,0.07);--text:#f0f0ff;--text2:rgba(240,240,255,0.55);--text3:rgba(240,240,255,0.3);--purple:#a855f7;--radius:14px}body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}body::before{content:'';position:fixed;top:-40%;left:-40%;width:180%;height:180%;background:radial-gradient(ellipse at 50% 30%,rgba(168,85,247,0.06) 0%,transparent 55%);pointer-events:none}.c{max-width:460px;margin:0 auto;padding:0 14px 32px;position:relative;z-index:1}.hdr{text-align:center;padding:28px 0 20px}.hdr h1{font-size:24px;font-weight:800;background:linear-gradient(135deg,#a855f7,#ec4899);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.hdr p{font-size:12px;color:var(--text2);margin-top:4px}.stats{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px}.stat{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center}.stat-val{font-size:22px;font-weight:800;color:var(--purple)}.stat-label{font-size:10px;color:var(--text2);text-transform:uppercase;margin-top:2px}.input-row{display:flex;gap:8px;margin-bottom:16px}.input-row input{flex:1;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:12px 14px;color:var(--text);font-size:14px;outline:none}.input-row input:focus{border-color:var(--purple)}.input-row input::placeholder{color:var(--text3)}.add-btn{background:linear-gradient(135deg,#a855f7,#ec4899);color:#fff;border:none;border-radius:10px;padding:0 18px;font-size:18px;font-weight:800;cursor:pointer}.filter-row{display:flex;gap:6px;margin-bottom:14px}.filter{flex:1;padding:8px;border-radius:8px;background:var(--card);border:1px solid var(--border);color:var(--text2);font-size:12px;font-weight:600;text-align:center;cursor:pointer}.filter.active{background:rgba(168,85,247,0.1);color:var(--purple)}.todo-list{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}.todo{display:flex;align-items:center;gap:12px;padding:14px 16px;border-bottom:1px solid rgba(255,255,255,0.03);cursor:pointer}.todo:last-child{border:none}.todo.done .t-text{text-decoration:line-through;opacity:.4}.check{width:22px;height:22px;border-radius:50%;border:2px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:12px;flex-shrink:0}.todo.done .check{background:var(--purple);border-color:var(--purple);color:#fff}.t-text{flex:1;font-size:14px;font-weight:500}.del{width:28px;height:28px;border-radius:8px;background:rgba(239,68,68,0.1);border:none;color:#ef4444;font-size:13px;cursor:pointer;display:flex;align-items:center;justify-content:center}.empty{text-align:center;padding:32px;color:var(--text3)}.ft{text-align:center;padding:16px}.ft-b{font-size:10px;font-weight:600;color:var(--text3);padding:5px 12px;border-radius:20px;background:rgba(168,85,247,0.04)}.clr{background:rgba(168,85,247,0.1);color:var(--purple);border:1px solid rgba(168,85,247,0.2);padding:8px 16px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;margin-top:12px}.clr:disabled{opacity:.3}@keyframes fadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.stat,.todo{animation:fadeIn .4s ease backwards}</style></head><body>
|
||||
<div class="c"><div class="hdr"><h1>📝 Todo App</h1><p>Express + Node.js</p></div>
|
||||
<div class="stats"><div class="stat"><div class="stat-val" id="total">0</div><div class="stat-label">Total</div></div><div class="stat"><div class="stat-val" id="active">0</div><div class="stat-label">Active</div></div><div class="stat"><div class="stat-val" id="done">0</div><div class="stat-label">Done</div></div></div>
|
||||
<div class="input-row"><input type="text" id="inp" placeholder="Add a task..." maxlength="100" onkeydown="if(event.key==='Enter')addTodo()"><button class="add-btn" onclick="addTodo()">+</button></div>
|
||||
<div class="filter-row"><div class="filter active" onclick="setFilter('all',this)">All</div><div class="filter" onclick="setFilter('active',this)">Active</div><div class="filter" onclick="setFilter('done',this)">Done</div></div>
|
||||
<div class="todo-list" id="list"></div>
|
||||
<div style="text-align:center"><button class="clr" id="clrBtn" onclick="clearDone()" disabled>Clear Completed</button></div>
|
||||
<div class="ft"><div class="ft-b">📝 Express · Node.js · WebToApp</div></div></div>
|
||||
<script>var todos=[],filter='all';function load(){fetch('/api/todos').then(function(r){return r.json()}).then(function(j){todos=j.data||[];render()})}function render(){var f=todos.filter(function(t){return filter==='all'?true:filter==='active'?!t.done:t.done});var list=document.getElementById('list');document.getElementById('total').textContent=todos.length;document.getElementById('active').textContent=todos.filter(function(t){return!t.done}).length;var dn=todos.filter(function(t){return t.done}).length;document.getElementById('done').textContent=dn;document.getElementById('clrBtn').disabled=dn===0;if(f.length===0){list.innerHTML='<div class="empty">🎉 No tasks</div>';return}list.innerHTML=f.map(function(t,i){return'<div class="todo'+(t.done?' done':'')+'" style="animation-delay:'+(i*.04)+'s"><div class="check" onclick="tog('+t.id+')">'+(t.done?'✓':'')+'</div><div class="t-text" onclick="tog('+t.id+')">'+esc(t.text)+'</div><button class="del" onclick="del('+t.id+')">×</button></div>'}).join('')}function esc(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML}function addTodo(){var v=document.getElementById('inp').value.trim();if(!v)return;fetch('/api/todos',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:v})}).then(function(r){return r.json()}).then(function(j){if(j.success){todos.unshift(j.data);document.getElementById('inp').value='';render()}})}function tog(id){fetch('/api/todos/'+id+'/toggle',{method:'PATCH'}).then(function(r){return r.json()}).then(function(j){if(j.success){var t=todos.find(function(x){return x.id===id});if(t)t.done=j.data.done;render()}})}function del(id){fetch('/api/todos/'+id,{method:'DELETE'}).then(function(r){if(r.ok){todos=todos.filter(function(t){return t.id!==id});render()}})}function clearDone(){fetch('/api/todos/completed/all',{method:'DELETE'}).then(function(r){if(r.ok){todos=todos.filter(function(t){return!t.done});render()}})}function setFilter(f,el){filter=f;document.querySelectorAll('.filter').forEach(function(e){e.classList.remove('active')});el.classList.add('active');render()}load();</script></body></html>`;
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "todo-app",
|
||||
"version": "1.0.0",
|
||||
"description": "待办事项应用 - 零依赖 Todo App,使用 Node.js 内置 http 模块",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {},
|
||||
"keywords": ["todo", "nodejs", "zero-dependency"],
|
||||
"author": "WebToApp"
|
||||
}
|
||||
148
app/src/main/assets/sample_projects/nodejs-express/server.js
Normal file
148
app/src/main/assets/sample_projects/nodejs-express/server.js
Normal file
@ -0,0 +1,148 @@
|
||||
|
||||
|
||||
|
||||
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const DATA_FILE = path.join(__dirname, 'todos.json');
|
||||
|
||||
const loadTodos = () => {
|
||||
try { if (fs.existsSync(DATA_FILE)) return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); } catch (e) { }
|
||||
return [
|
||||
{ id: 1, text: '欢迎使用待办事项 👋', done: false, createdAt: Date.now() },
|
||||
{ id: 2, text: '点击圆圈完成任务 ✓', done: true, createdAt: Date.now() - 1000 },
|
||||
{ id: 3, text: '点击 × 删除任务', done: false, createdAt: Date.now() - 2000 },
|
||||
{ id: 4, text: '支持数据持久化', done: false, createdAt: Date.now() - 3000 }
|
||||
];
|
||||
};
|
||||
const saveTodos = (todos) => fs.writeFileSync(DATA_FILE, JSON.stringify(todos, null, 2));
|
||||
let todos = loadTodos();
|
||||
let nextId = Math.max(...todos.map(t => t.id), 0) + 1;
|
||||
|
||||
function parseBody(req) {
|
||||
return new Promise((resolve) => {
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk);
|
||||
req.on('end', () => { try { resolve(JSON.parse(body)); } catch (e) { resolve({}); } });
|
||||
});
|
||||
}
|
||||
|
||||
function sendJson(res, data, status = 200) {
|
||||
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
const p = url.pathname, m = req.method;
|
||||
|
||||
if (m === 'GET' && p === '/') {
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
return res.end(getPage());
|
||||
}
|
||||
if (m === 'GET' && p === '/api/todos') return sendJson(res, { success: true, data: todos });
|
||||
if (m === 'POST' && p === '/api/todos') {
|
||||
const { text } = await parseBody(req);
|
||||
if (!text?.trim()) return sendJson(res, { success: false }, 400);
|
||||
const todo = { id: nextId++, text: text.trim(), done: false, createdAt: Date.now() };
|
||||
todos.unshift(todo); saveTodos(todos);
|
||||
return sendJson(res, { success: true, data: todo });
|
||||
}
|
||||
const tog = p.match(/^\/api\/todos\/(\d+)\/toggle$/);
|
||||
if (m === 'PATCH' && tog) {
|
||||
const todo = todos.find(t => t.id === parseInt(tog[1]));
|
||||
if (!todo) return sendJson(res, { success: false }, 404);
|
||||
todo.done = !todo.done; saveTodos(todos);
|
||||
return sendJson(res, { success: true, data: todo });
|
||||
}
|
||||
if (m === 'DELETE' && p === '/api/todos/completed/all') {
|
||||
todos = todos.filter(t => !t.done); saveTodos(todos);
|
||||
return sendJson(res, { success: true });
|
||||
}
|
||||
const del = p.match(/^\/api\/todos\/(\d+)$/);
|
||||
if (m === 'DELETE' && del) {
|
||||
const i = todos.findIndex(t => t.id === parseInt(del[1]));
|
||||
if (i === -1) return sendJson(res, { success: false }, 404);
|
||||
todos.splice(i, 1); saveTodos(todos);
|
||||
return sendJson(res, { success: true });
|
||||
}
|
||||
sendJson(res, { error: 'Not Found' }, 404);
|
||||
});
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => console.log(`✅ Todo on http://0.0.0.0:${PORT}`));
|
||||
|
||||
function getPage() {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>待办事项</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{--bg:#0a0a1a;--card:rgba(255,255,255,0.04);--border:rgba(255,255,255,0.07);--text:#f0f0ff;--text2:rgba(240,240,255,0.55);--text3:rgba(240,240,255,0.3);--purple:#a855f7;--radius:14px}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
||||
body::before{content:'';position:fixed;top:-40%;left:-40%;width:180%;height:180%;background:radial-gradient(ellipse at 50% 30%,rgba(168,85,247,0.06) 0%,transparent 55%);pointer-events:none}
|
||||
.c{max-width:460px;margin:0 auto;padding:0 14px 32px;position:relative;z-index:1}
|
||||
.hdr{text-align:center;padding:28px 0 20px}
|
||||
.hdr h1{font-size:24px;font-weight:800;background:linear-gradient(135deg,#a855f7,#ec4899);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.hdr p{font-size:12px;color:var(--text2);margin-top:4px}
|
||||
.stats{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px}
|
||||
.stat{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center}
|
||||
.stat-val{font-size:22px;font-weight:800;color:var(--purple)}
|
||||
.stat-label{font-size:10px;color:var(--text2);text-transform:uppercase;letter-spacing:.5px;margin-top:2px}
|
||||
.input-row{display:flex;gap:8px;margin-bottom:16px}
|
||||
.input-row input{flex:1;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:12px 14px;color:var(--text);font-size:14px;outline:none}
|
||||
.input-row input:focus{border-color:var(--purple)}
|
||||
.input-row input::placeholder{color:var(--text3)}
|
||||
.add-btn{background:linear-gradient(135deg,#a855f7,#ec4899);color:#fff;border:none;border-radius:10px;padding:0 18px;font-size:18px;font-weight:800;cursor:pointer}
|
||||
.add-btn:active{transform:scale(.93)}
|
||||
.filter-row{display:flex;gap:6px;margin-bottom:14px}
|
||||
.filter{flex:1;padding:8px;border-radius:8px;background:var(--card);border:1px solid var(--border);color:var(--text2);font-size:12px;font-weight:600;text-align:center;cursor:pointer}
|
||||
.filter.active{background:rgba(168,85,247,0.1);color:var(--purple);border-color:rgba(168,85,247,0.2)}
|
||||
.todo-list{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
|
||||
.todo{display:flex;align-items:center;gap:12px;padding:14px 16px;border-bottom:1px solid rgba(255,255,255,0.03);cursor:pointer}
|
||||
.todo:last-child{border:none}
|
||||
.todo.done .t-text{text-decoration:line-through;opacity:.4}
|
||||
.check{width:22px;height:22px;border-radius:50%;border:2px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:12px;flex-shrink:0;transition:all .2s}
|
||||
.todo.done .check{background:var(--purple);border-color:var(--purple);color:#fff}
|
||||
.t-text{flex:1;font-size:14px;font-weight:500}
|
||||
.del{width:28px;height:28px;border-radius:8px;background:rgba(239,68,68,0.1);border:none;color:#ef4444;font-size:13px;cursor:pointer;display:flex;align-items:center;justify-content:center}
|
||||
.empty{text-align:center;padding:32px;color:var(--text3)}
|
||||
.ft{text-align:center;padding:16px}
|
||||
.ft-b{font-size:10px;font-weight:600;color:var(--text3);padding:5px 12px;border-radius:20px;background:rgba(168,85,247,0.04);border:1px solid rgba(168,85,247,0.08)}
|
||||
.clr{background:rgba(168,85,247,0.1);color:var(--purple);border:1px solid rgba(168,85,247,0.2);padding:8px 16px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;margin-top:12px}
|
||||
.clr:disabled{opacity:.3;cursor:not-allowed}
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
|
||||
.stat,.todo{animation:fadeIn .4s ease backwards}
|
||||
</style></head><body>
|
||||
<div class="c">
|
||||
<div class="hdr"><h1>📝 待办事项</h1><p>Express + Node.js 驱动</p></div>
|
||||
<div class="stats"><div class="stat"><div class="stat-val" id="total">0</div><div class="stat-label">全部</div></div><div class="stat"><div class="stat-val" id="active">0</div><div class="stat-label">待办</div></div><div class="stat"><div class="stat-val" id="done">0</div><div class="stat-label">完成</div></div></div>
|
||||
<div class="input-row"><input type="text" id="inp" placeholder="添加新任务..." maxlength="100" onkeydown="if(event.key==='Enter')addTodo()"><button class="add-btn" onclick="addTodo()">+</button></div>
|
||||
<div class="filter-row"><div class="filter active" onclick="setFilter('all',this)">全部</div><div class="filter" onclick="setFilter('active',this)">待办</div><div class="filter" onclick="setFilter('done',this)">已完成</div></div>
|
||||
<div class="todo-list" id="list"></div>
|
||||
<div style="text-align:center"><button class="clr" id="clrBtn" onclick="clearDone()" disabled>清除已完成</button></div>
|
||||
<div class="ft"><div class="ft-b">📝 Express · Node.js · WebToApp</div></div>
|
||||
</div>
|
||||
<script>
|
||||
var todos=[],filter='all';
|
||||
function load(){fetch('/api/todos').then(function(r){return r.json()}).then(function(j){todos=j.data||[];render()})}
|
||||
function render(){
|
||||
var f=todos.filter(function(t){return filter==='all'?true:filter==='active'?!t.done:t.done});
|
||||
var list=document.getElementById('list');
|
||||
document.getElementById('total').textContent=todos.length;
|
||||
document.getElementById('active').textContent=todos.filter(function(t){return !t.done}).length;
|
||||
var dn=todos.filter(function(t){return t.done}).length;
|
||||
document.getElementById('done').textContent=dn;
|
||||
document.getElementById('clrBtn').disabled=dn===0;
|
||||
if(f.length===0){list.innerHTML='<div class="empty">🎉 '+(filter==='all'?'暂无任务':'没有匹配')+'</div>';return}
|
||||
list.innerHTML=f.map(function(t,i){return '<div class="todo'+(t.done?' done':'')+'" style="animation-delay:'+(i*0.04)+'s"><div class="check" onclick="tog('+t.id+')">'+(t.done?'✓':'')+'</div><div class="t-text" onclick="tog('+t.id+')">'+esc(t.text)+'</div><button class="del" onclick="del('+t.id+')">×</button></div>'}).join('')}
|
||||
function esc(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML}
|
||||
function addTodo(){var v=document.getElementById('inp').value.trim();if(!v)return;fetch('/api/todos',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:v})}).then(function(r){return r.json()}).then(function(j){if(j.success){todos.unshift(j.data);document.getElementById('inp').value='';render()}})}
|
||||
function tog(id){fetch('/api/todos/'+id+'/toggle',{method:'PATCH'}).then(function(r){return r.json()}).then(function(j){if(j.success){var t=todos.find(function(x){return x.id===id});if(t)t.done=j.data.done;render()}})}
|
||||
function del(id){fetch('/api/todos/'+id,{method:'DELETE'}).then(function(r){if(r.ok){todos=todos.filter(function(t){return t.id!==id});render()}})}
|
||||
function clearDone(){fetch('/api/todos/completed/all',{method:'DELETE'}).then(function(r){if(r.ok){todos=todos.filter(function(t){return !t.done});render()}})}
|
||||
function setFilter(f,el){filter=f;document.querySelectorAll('.filter').forEach(function(e){e.classList.remove('active')});el.classList.add('active');render()}
|
||||
load();
|
||||
</script></body></html>`;
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "system-dashboard",
|
||||
"version": "1.0.0",
|
||||
"main": "server.js",
|
||||
"scripts": { "start": "node server.js" },
|
||||
"dependencies": {}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
|
||||
|
||||
|
||||
|
||||
const http = require('http');
|
||||
const os = require('os');
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const stats = { requestCount: 0, startTime: Date.now() };
|
||||
function getStats() { stats.requestCount++; const cpus = os.cpus(); const cu = cpus.map(c => { const t = Object.values(c.times).reduce((a, b) => a + b, 0); return Math.round((1 - c.times.idle / t) * 100) }); const avg = Math.round(cu.reduce((a, b) => a + b, 0) / cu.length); const tm = os.totalmem(), fm = os.freemem(), um = tm - fm, mp = Math.round(um / tm * 100); const p = process.memoryUsage(); return { cpu: { cores: cpus.length, usage: avg, perCore: cu }, memory: { total: fB(tm), used: fB(um), free: fB(fm), percent: mp }, process: { heap: fB(p.heapUsed), rss: fB(p.rss), uptime: fT(process.uptime()) }, server: { requests: stats.requestCount, uptime: fT((Date.now() - stats.startTime) / 1000) }, system: { platform: os.platform(), arch: os.arch(), node: process.version } } }
|
||||
function fB(b) { if (b < 1048576) return (b / 1024).toFixed(0) + ' KB'; if (b < 1073741824) return (b / 1048576).toFixed(1) + ' MB'; return (b / 1073741824).toFixed(2) + ' GB' }
|
||||
function fT(s) { const m = Math.floor(s / 60), h = Math.floor(m / 60); if (h > 0) return h + 'س ' + m % 60 + 'د'; if (m > 0) return m + 'د ' + Math.floor(s % 60) + 'ث'; return Math.floor(s) + 'ث' }
|
||||
const server = http.createServer((req, res) => {
|
||||
const p = new URL(req.url, `http://${req.headers.host}`).pathname;
|
||||
if (p === '/api/stats') { res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify(getStats())) }
|
||||
if (p === '/') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); return res.end(getPage()) }
|
||||
res.writeHead(404); res.end('Not Found');
|
||||
});
|
||||
server.listen(PORT, '0.0.0.0', () => console.log('Monitor on ' + PORT));
|
||||
|
||||
function getPage() {
|
||||
return `<!DOCTYPE html><html lang="ar" dir="rtl"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>مراقبة النظام</title>
|
||||
<style>*{margin:0;padding:0;box-sizing:border-box}:root{--bg:#080818;--card:rgba(255,255,255,0.035);--border:rgba(255,255,255,0.07);--text:#f0f0ff;--text2:rgba(240,240,255,0.55);--text3:rgba(240,240,255,0.3);--cyan:#00d4ff;--radius:14px}body{font-family:-apple-system,BlinkMacSystemFont,Tahoma,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;direction:rtl}body::before{content:'';position:fixed;top:-40%;left:-40%;width:180%;height:180%;background:radial-gradient(ellipse at 50% 30%,rgba(0,212,255,0.05) 0%,transparent 55%);pointer-events:none}.c{max-width:460px;margin:0 auto;padding:0 14px 32px;position:relative;z-index:1}.hdr{padding:20px 2px;display:flex;justify-content:space-between;align-items:center}.hdr h1{font-size:22px;font-weight:800;background:linear-gradient(135deg,#00d4ff,#7b2ff7);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.hdr p{font-size:12px;color:var(--text2)}.badge{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:5px 12px;border-radius:20px;background:rgba(0,212,255,0.1);color:var(--cyan)}.badge .dot{width:7px;height:7px;border-radius:50%;background:#00ff88;animation:p 2s infinite}@keyframes p{0%,100%{opacity:1}50%{opacity:.4}}.row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:14px}.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:14px}.card-h{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.card-t{font-size:15px;font-weight:700}.card-b{font-size:10px;font-weight:600;padding:3px 9px;border-radius:16px;background:rgba(0,212,255,0.1);color:var(--cyan)}.metric{font-size:32px;font-weight:800;margin-bottom:4px}.prog{height:6px;background:rgba(255,255,255,0.05);border-radius:3px;margin-top:8px;overflow:hidden}.prog-fill{height:100%;border-radius:3px;transition:width .5s}.cpu-fill{background:linear-gradient(90deg,#00d4ff,#0099ff)}.mem-fill{background:linear-gradient(90deg,#7b2ff7,#f107a3)}.cores{display:flex;gap:4px;margin-top:10px;direction:ltr}.core{flex:1;min-width:20px;height:40px;background:rgba(255,255,255,0.03);border-radius:4px;position:relative;overflow:hidden}.core-fill{position:absolute;bottom:0;left:0;right:0;background:linear-gradient(0deg,#00d4ff,#00ff88);transition:height .5s}.core-label{position:absolute;bottom:2px;left:50%;transform:translateX(-50%);font-size:8px;color:#fff}.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.info{background:rgba(255,255,255,0.02);padding:10px;border-radius:8px}.info-label{font-size:10px;color:var(--text3);margin-bottom:2px}.info-val{font-size:13px;font-weight:600}.ft{text-align:center;padding:12px}.ft-b{font-size:10px;font-weight:600;color:var(--text3);padding:5px 12px;border-radius:20px;background:rgba(0,212,255,0.04)}@keyframes fadeIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}.card{animation:fadeIn .5s ease backwards}</style></head><body>
|
||||
<div class="c"><div class="hdr"><div><h1>⚡ مراقبة النظام</h1><p>Fastify عالي الأداء</p></div><div class="badge"><span class="dot"></span>مباشر</div></div>
|
||||
<div class="row"><div class="card"><div class="card-h"><span class="card-t">💻 المعالج</span><span class="card-b" id="cpuCores">-</span></div><div class="metric" id="cpuVal">--%</div><div class="prog"><div class="prog-fill cpu-fill" id="cpuBar" style="width:0"></div></div><div class="cores" id="cores"></div></div><div class="card"><div class="card-h"><span class="card-t">🧠 الذاكرة</span><span class="card-b" id="memDetail">-</span></div><div class="metric" id="memVal">--%</div><div class="prog"><div class="prog-fill mem-fill" id="memBar" style="width:0"></div></div><div class="info-grid" style="margin-top:10px"><div class="info"><div class="info-label">مستخدم</div><div class="info-val" id="memUsed">-</div></div><div class="info"><div class="info-label">متاح</div><div class="info-val" id="memFree">-</div></div></div></div></div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">🚀 الخادم</span><span class="card-b" style="background:rgba(0,255,136,0.1);color:#00ff88">● يعمل</span></div><div class="info-grid"><div class="info"><div class="info-label">التشغيل</div><div class="info-val" id="uptime">-</div></div><div class="info"><div class="info-label">الطلبات</div><div class="info-val" id="reqs">-</div></div><div class="info"><div class="info-label">Heap</div><div class="info-val" id="heap">-</div></div><div class="info"><div class="info-label">RSS</div><div class="info-val" id="rss">-</div></div></div></div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">📊 النظام</span><span class="card-b">معلومات</span></div><div class="info-grid"><div class="info"><div class="info-label">المنصة</div><div class="info-val" id="plat">-</div></div><div class="info"><div class="info-label">الهندسة</div><div class="info-val" id="arch">-</div></div><div class="info"><div class="info-label">Node.js</div><div class="info-val" id="node">-</div></div><div class="info"><div class="info-label">PID</div><div class="info-val">${process.pid}</div></div></div></div>
|
||||
<div class="ft"><div class="ft-b">⚡ Fastify · Node.js · WebToApp</div></div></div>
|
||||
<script>function fetchStats(){fetch('/api/stats').then(function(r){return r.json()}).then(function(d){document.getElementById('cpuVal').textContent=d.cpu.usage+'%';document.getElementById('cpuCores').textContent=d.cpu.cores+' أنوية';document.getElementById('cpuBar').style.width=d.cpu.usage+'%';var ch='';d.cpu.perCore.forEach(function(u){ch+='<div class="core"><div class="core-fill" style="height:'+u+'%"></div><div class="core-label">'+u+'</div></div>'});document.getElementById('cores').innerHTML=ch;document.getElementById('memVal').textContent=d.memory.percent+'%';document.getElementById('memDetail').textContent=d.memory.used+'/'+d.memory.total;document.getElementById('memBar').style.width=d.memory.percent+'%';document.getElementById('memUsed').textContent=d.memory.used;document.getElementById('memFree').textContent=d.memory.free;document.getElementById('uptime').textContent=d.server.uptime;document.getElementById('reqs').textContent=d.server.requests;document.getElementById('heap').textContent=d.process.heap;document.getElementById('rss').textContent=d.process.rss;document.getElementById('plat').textContent=d.system.platform;document.getElementById('arch').textContent=d.system.arch;document.getElementById('node').textContent=d.system.node}).catch(function(){})}fetchStats();setInterval(fetchStats,2000)</script></body></html>`;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "system-dashboard",
|
||||
"version": "1.0.0",
|
||||
"description": "System Monitor Dashboard - Zero dependency, uses Node.js built-in http module",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
|
||||
|
||||
|
||||
|
||||
const http = require('http');
|
||||
const os = require('os');
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const stats = { requestCount: 0, startTime: Date.now() };
|
||||
|
||||
function getStats() {
|
||||
stats.requestCount++;
|
||||
const cpus = os.cpus();
|
||||
const cpuUsage = cpus.map(cpu => { const t = Object.values(cpu.times).reduce((a, b) => a + b, 0); return Math.round((1 - cpu.times.idle / t) * 100); });
|
||||
const avgCpu = Math.round(cpuUsage.reduce((a, b) => a + b, 0) / cpuUsage.length);
|
||||
const totalMem = os.totalmem(), freeMem = os.freemem(), usedMem = totalMem - freeMem, memPct = Math.round(usedMem / totalMem * 100);
|
||||
const proc = process.memoryUsage();
|
||||
return { cpu: { cores: cpus.length, usage: avgCpu, perCore: cpuUsage }, memory: { total: fmtB(totalMem), used: fmtB(usedMem), free: fmtB(freeMem), percent: memPct }, process: { heap: fmtB(proc.heapUsed), rss: fmtB(proc.rss), uptime: fmtT(process.uptime()) }, server: { requests: stats.requestCount, uptime: fmtT((Date.now() - stats.startTime) / 1000) }, system: { platform: os.platform(), arch: os.arch(), node: process.version } };
|
||||
}
|
||||
function fmtB(b) { if (b < 1024 * 1024) return (b / 1024).toFixed(0) + ' KB'; if (b < 1024 * 1024 * 1024) return (b / 1024 / 1024).toFixed(1) + ' MB'; return (b / 1024 / 1024 / 1024).toFixed(2) + ' GB' }
|
||||
function fmtT(s) { const m = Math.floor(s / 60), h = Math.floor(m / 60); if (h > 0) return h + 'h ' + m % 60 + 'm'; if (m > 0) return m + 'm ' + Math.floor(s % 60) + 's'; return Math.floor(s) + 's' }
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const p = new URL(req.url, `http://${req.headers.host}`).pathname;
|
||||
if (p === '/api/stats') { res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify(getStats())) }
|
||||
if (p === '/') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); return res.end(getPage()) }
|
||||
res.writeHead(404); res.end('Not Found');
|
||||
});
|
||||
server.listen(PORT, '0.0.0.0', () => console.log('Monitor on http://0.0.0.0:' + PORT));
|
||||
|
||||
function getPage() {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>System Monitor</title>
|
||||
<style>*{margin:0;padding:0;box-sizing:border-box}:root{--bg:#080818;--card:rgba(255,255,255,0.035);--border:rgba(255,255,255,0.07);--text:#f0f0ff;--text2:rgba(240,240,255,0.55);--text3:rgba(240,240,255,0.3);--cyan:#00d4ff;--radius:14px}body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}body::before{content:'';position:fixed;top:-40%;left:-40%;width:180%;height:180%;background:radial-gradient(ellipse at 50% 30%,rgba(0,212,255,0.05) 0%,transparent 55%);pointer-events:none}.c{max-width:460px;margin:0 auto;padding:0 14px 32px;position:relative;z-index:1}.hdr{padding:20px 2px;display:flex;justify-content:space-between;align-items:center}.hdr h1{font-size:22px;font-weight:800;background:linear-gradient(135deg,#00d4ff,#7b2ff7);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.hdr p{font-size:12px;color:var(--text2);margin-top:2px}.badge{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:5px 12px;border-radius:20px;background:rgba(0,212,255,0.1);color:var(--cyan)}.badge .dot{width:7px;height:7px;border-radius:50%;background:#00ff88;box-shadow:0 0 10px #00ff88;animation:p 2s infinite}@keyframes p{0%,100%{opacity:1}50%{opacity:.4}}.row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:14px}.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:14px}.card-h{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.card-t{font-size:15px;font-weight:700}.card-b{font-size:10px;font-weight:600;padding:3px 9px;border-radius:16px;background:rgba(0,212,255,0.1);color:var(--cyan)}.metric{font-size:32px;font-weight:800;letter-spacing:-1px;margin-bottom:4px}.metric-sub{font-size:11px;color:var(--text2)}.prog{height:6px;background:rgba(255,255,255,0.05);border-radius:3px;margin-top:8px;overflow:hidden}.prog-fill{height:100%;border-radius:3px;transition:width .5s}.cpu-fill{background:linear-gradient(90deg,#00d4ff,#0099ff)}.mem-fill{background:linear-gradient(90deg,#7b2ff7,#f107a3)}.cores{display:flex;gap:4px;margin-top:10px;flex-wrap:wrap}.core{flex:1;min-width:20px;height:40px;background:rgba(255,255,255,0.03);border-radius:4px;position:relative;overflow:hidden}.core-fill{position:absolute;bottom:0;left:0;right:0;background:linear-gradient(0deg,#00d4ff,#00ff88);transition:height .5s}.core-label{position:absolute;bottom:2px;left:50%;transform:translateX(-50%);font-size:8px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.5)}.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.info{background:rgba(255,255,255,0.02);padding:10px;border-radius:8px}.info-label{font-size:10px;color:var(--text3);margin-bottom:2px}.info-val{font-size:13px;font-weight:600}.ft{text-align:center;padding:12px}.ft-b{font-size:10px;font-weight:600;color:var(--text3);padding:5px 12px;border-radius:20px;background:rgba(0,212,255,0.04)}@keyframes fadeIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}.card{animation:fadeIn .5s ease backwards}</style></head><body>
|
||||
<div class="c"><div class="hdr"><div><h1>⚡ System Monitor</h1><p>Fastify High-Performance</p></div><div class="badge"><span class="dot"></span>Live</div></div>
|
||||
<div class="row"><div class="card"><div class="card-h"><span class="card-t">💻 CPU</span><span class="card-b" id="cpuCores">-</span></div><div class="metric" id="cpuVal">--%</div><div class="prog"><div class="prog-fill cpu-fill" id="cpuBar" style="width:0"></div></div><div class="cores" id="cores"></div></div><div class="card"><div class="card-h"><span class="card-t">🧠 Memory</span><span class="card-b" id="memDetail">-</span></div><div class="metric" id="memVal">--%</div><div class="prog"><div class="prog-fill mem-fill" id="memBar" style="width:0"></div></div><div class="info-grid" style="margin-top:10px"><div class="info"><div class="info-label">Used</div><div class="info-val" id="memUsed">-</div></div><div class="info"><div class="info-label">Free</div><div class="info-val" id="memFree">-</div></div></div></div></div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">🚀 Server</span><span class="card-b" style="background:rgba(0,255,136,0.1);color:#00ff88">● Running</span></div><div class="info-grid"><div class="info"><div class="info-label">Uptime</div><div class="info-val" id="uptime">-</div></div><div class="info"><div class="info-label">Requests</div><div class="info-val" id="reqs">-</div></div><div class="info"><div class="info-label">Heap</div><div class="info-val" id="heap">-</div></div><div class="info"><div class="info-label">RSS</div><div class="info-val" id="rss">-</div></div></div></div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">📊 System</span><span class="card-b">Info</span></div><div class="info-grid"><div class="info"><div class="info-label">Platform</div><div class="info-val" id="plat">-</div></div><div class="info"><div class="info-label">Arch</div><div class="info-val" id="arch">-</div></div><div class="info"><div class="info-label">Node.js</div><div class="info-val" id="node">-</div></div><div class="info"><div class="info-label">PID</div><div class="info-val">${process.pid}</div></div></div></div>
|
||||
<div class="ft"><div class="ft-b">⚡ Fastify · Node.js · WebToApp</div></div></div>
|
||||
<script>function fetchStats(){fetch('/api/stats').then(function(r){return r.json()}).then(function(d){document.getElementById('cpuVal').textContent=d.cpu.usage+'%';document.getElementById('cpuCores').textContent=d.cpu.cores+' Cores';document.getElementById('cpuBar').style.width=d.cpu.usage+'%';var ch='';d.cpu.perCore.forEach(function(u){ch+='<div class="core"><div class="core-fill" style="height:'+u+'%"></div><div class="core-label">'+u+'</div></div>'});document.getElementById('cores').innerHTML=ch;document.getElementById('memVal').textContent=d.memory.percent+'%';document.getElementById('memDetail').textContent=d.memory.used+'/'+d.memory.total;document.getElementById('memBar').style.width=d.memory.percent+'%';document.getElementById('memUsed').textContent=d.memory.used;document.getElementById('memFree').textContent=d.memory.free;document.getElementById('uptime').textContent=d.server.uptime;document.getElementById('reqs').textContent=d.server.requests;document.getElementById('heap').textContent=d.process.heap;document.getElementById('rss').textContent=d.process.rss;document.getElementById('plat').textContent=d.system.platform;document.getElementById('arch').textContent=d.system.arch;document.getElementById('node').textContent=d.system.node}).catch(function(){})}fetchStats();setInterval(fetchStats,2000)</script></body></html>`;
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "system-dashboard",
|
||||
"version": "1.0.0",
|
||||
"description": "实时系统监控仪表盘 - 零依赖,使用 Node.js 内置 http 模块",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {},
|
||||
"keywords": ["dashboard", "monitor", "nodejs", "zero-dependency"],
|
||||
"author": "WebToApp"
|
||||
}
|
||||
113
app/src/main/assets/sample_projects/nodejs-fastify/server.js
Normal file
113
app/src/main/assets/sample_projects/nodejs-fastify/server.js
Normal file
@ -0,0 +1,113 @@
|
||||
|
||||
|
||||
|
||||
|
||||
const http = require('http');
|
||||
const os = require('os');
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const stats = { requestCount: 0, startTime: Date.now() };
|
||||
|
||||
function getStats() {
|
||||
stats.requestCount++;
|
||||
const cpus = os.cpus();
|
||||
const cpuUsage = cpus.map(cpu => {
|
||||
const total = Object.values(cpu.times).reduce((a, b) => a + b, 0);
|
||||
return Math.round((1 - cpu.times.idle / total) * 100);
|
||||
});
|
||||
const avgCpu = Math.round(cpuUsage.reduce((a, b) => a + b, 0) / cpuUsage.length);
|
||||
const totalMem = os.totalmem(), freeMem = os.freemem(), usedMem = totalMem - freeMem;
|
||||
const memPct = Math.round((usedMem / totalMem) * 100);
|
||||
const proc = process.memoryUsage();
|
||||
return {
|
||||
cpu: { cores: cpus.length, usage: avgCpu, perCore: cpuUsage },
|
||||
memory: { total: fmtB(totalMem), used: fmtB(usedMem), free: fmtB(freeMem), percent: memPct },
|
||||
process: { heap: fmtB(proc.heapUsed), rss: fmtB(proc.rss), uptime: fmtT(process.uptime()) },
|
||||
server: { requests: stats.requestCount, uptime: fmtT((Date.now() - stats.startTime) / 1000) },
|
||||
system: { platform: os.platform(), arch: os.arch(), node: process.version }
|
||||
};
|
||||
}
|
||||
|
||||
function fmtB(b) { if (b < 1024 * 1024) return (b / 1024).toFixed(0) + ' KB'; if (b < 1024 * 1024 * 1024) return (b / 1024 / 1024).toFixed(1) + ' MB'; return (b / 1024 / 1024 / 1024).toFixed(2) + ' GB'; }
|
||||
function fmtT(s) { const m = Math.floor(s / 60), h = Math.floor(m / 60); if (h > 0) return h + '时' + m % 60 + '分'; if (m > 0) return m + '分' + Math.floor(s % 60) + '秒'; return Math.floor(s) + '秒'; }
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const p = new URL(req.url, `http://${req.headers.host}`).pathname;
|
||||
if (p === '/api/stats') { res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify(getStats())); }
|
||||
if (p === '/') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); return res.end(getPage()); }
|
||||
res.writeHead(404); res.end('Not Found');
|
||||
});
|
||||
server.listen(PORT, '0.0.0.0', () => console.log(`⚡ Monitor on http://0.0.0.0:${PORT}`));
|
||||
|
||||
function getPage() {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>系统监控</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{--bg:#080818;--card:rgba(255,255,255,0.035);--border:rgba(255,255,255,0.07);--text:#f0f0ff;--text2:rgba(240,240,255,0.55);--text3:rgba(240,240,255,0.3);--cyan:#00d4ff;--radius:14px}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
||||
body::before{content:'';position:fixed;top:-40%;left:-40%;width:180%;height:180%;background:radial-gradient(ellipse at 50% 30%,rgba(0,212,255,0.05) 0%,transparent 55%);pointer-events:none}
|
||||
.c{max-width:460px;margin:0 auto;padding:0 14px 32px;position:relative;z-index:1}
|
||||
.hdr{padding:20px 2px;display:flex;justify-content:space-between;align-items:center}
|
||||
.hdr h1{font-size:22px;font-weight:800;background:linear-gradient(135deg,#00d4ff,#7b2ff7);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.hdr p{font-size:12px;color:var(--text2);margin-top:2px}
|
||||
.badge{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:5px 12px;border-radius:20px;background:rgba(0,212,255,0.1);color:var(--cyan)}
|
||||
.badge .dot{width:7px;height:7px;border-radius:50%;background:#00ff88;box-shadow:0 0 10px #00ff88;animation:p 2s infinite}
|
||||
@keyframes p{0%,100%{opacity:1}50%{opacity:.4}}
|
||||
.row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:14px}
|
||||
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:14px;backdrop-filter:blur(16px)}
|
||||
.card-h{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
|
||||
.card-t{font-size:15px;font-weight:700}
|
||||
.card-b{font-size:10px;font-weight:600;padding:3px 9px;border-radius:16px;background:rgba(0,212,255,0.1);color:var(--cyan)}
|
||||
.metric{font-size:32px;font-weight:800;letter-spacing:-1px;margin-bottom:4px}
|
||||
.metric-sub{font-size:11px;color:var(--text2)}
|
||||
.prog{height:6px;background:rgba(255,255,255,0.05);border-radius:3px;margin-top:8px;overflow:hidden}
|
||||
.prog-fill{height:100%;border-radius:3px;transition:width .5s}
|
||||
.cpu-fill{background:linear-gradient(90deg,#00d4ff,#0099ff)}
|
||||
.mem-fill{background:linear-gradient(90deg,#7b2ff7,#f107a3)}
|
||||
.cores{display:flex;gap:4px;margin-top:10px;flex-wrap:wrap}
|
||||
.core{flex:1;min-width:20px;height:40px;background:rgba(255,255,255,0.03);border-radius:4px;position:relative;overflow:hidden}
|
||||
.core-fill{position:absolute;bottom:0;left:0;right:0;background:linear-gradient(0deg,#00d4ff,#00ff88);transition:height .5s;border-radius:0 0 4px 4px}
|
||||
.core-label{position:absolute;bottom:2px;left:50%;transform:translateX(-50%);font-size:8px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.5)}
|
||||
.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
||||
.info{background:rgba(255,255,255,0.02);padding:10px;border-radius:8px}
|
||||
.info-label{font-size:10px;color:var(--text3);margin-bottom:2px}
|
||||
.info-val{font-size:13px;font-weight:600}
|
||||
.ft{text-align:center;padding:12px}
|
||||
.ft-b{font-size:10px;font-weight:600;color:var(--text3);padding:5px 12px;border-radius:20px;background:rgba(0,212,255,0.04);border:1px solid rgba(0,212,255,0.08)}
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}
|
||||
.card{animation:fadeIn .5s ease backwards}
|
||||
</style></head><body>
|
||||
<div class="c">
|
||||
<div class="hdr"><div><h1>⚡ 系统监控</h1><p>Fastify 高性能服务</p></div><div class="badge"><span class="dot"></span>实时更新</div></div>
|
||||
<div class="row">
|
||||
<div class="card"><div class="card-h"><span class="card-t">💻 CPU</span><span class="card-b" id="cpuCores">-</span></div><div class="metric" id="cpuVal">--%</div><div class="prog"><div class="prog-fill cpu-fill" id="cpuBar" style="width:0"></div></div><div class="cores" id="cores"></div></div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">🧠 内存</span><span class="card-b" id="memDetail">-</span></div><div class="metric" id="memVal">--%</div><div class="prog"><div class="prog-fill mem-fill" id="memBar" style="width:0"></div></div><div class="info-grid" style="margin-top:10px"><div class="info"><div class="info-label">已用</div><div class="info-val" id="memUsed">-</div></div><div class="info"><div class="info-label">可用</div><div class="info-val" id="memFree">-</div></div></div></div>
|
||||
</div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">🚀 服务器</span><span class="card-b" style="background:rgba(0,255,136,0.1);color:#00ff88">● 运行中</span></div><div class="info-grid"><div class="info"><div class="info-label">运行时间</div><div class="info-val" id="uptime">-</div></div><div class="info"><div class="info-label">请求数</div><div class="info-val" id="reqs">-</div></div><div class="info"><div class="info-label">堆内存</div><div class="info-val" id="heap">-</div></div><div class="info"><div class="info-label">RSS</div><div class="info-val" id="rss">-</div></div></div></div>
|
||||
<div class="card"><div class="card-h"><span class="card-t">📊 系统</span><span class="card-b">信息</span></div><div class="info-grid"><div class="info"><div class="info-label">平台</div><div class="info-val" id="plat">-</div></div><div class="info"><div class="info-label">架构</div><div class="info-val" id="arch">-</div></div><div class="info"><div class="info-label">Node.js</div><div class="info-val" id="node">-</div></div><div class="info"><div class="info-label">PID</div><div class="info-val">${process.pid}</div></div></div></div>
|
||||
<div class="ft"><div class="ft-b">⚡ Fastify · Node.js · WebToApp</div></div>
|
||||
</div>
|
||||
<script>
|
||||
function fetchStats(){
|
||||
fetch('/api/stats').then(function(r){return r.json()}).then(function(d){
|
||||
document.getElementById('cpuVal').textContent=d.cpu.usage+'%';
|
||||
document.getElementById('cpuCores').textContent=d.cpu.cores+' 核心';
|
||||
document.getElementById('cpuBar').style.width=d.cpu.usage+'%';
|
||||
var ch='';d.cpu.perCore.forEach(function(u){ch+='<div class="core"><div class="core-fill" style="height:'+u+'%"></div><div class="core-label">'+u+'</div></div>'});
|
||||
document.getElementById('cores').innerHTML=ch;
|
||||
document.getElementById('memVal').textContent=d.memory.percent+'%';
|
||||
document.getElementById('memDetail').textContent=d.memory.used+'/'+d.memory.total;
|
||||
document.getElementById('memBar').style.width=d.memory.percent+'%';
|
||||
document.getElementById('memUsed').textContent=d.memory.used;
|
||||
document.getElementById('memFree').textContent=d.memory.free;
|
||||
document.getElementById('uptime').textContent=d.server.uptime;
|
||||
document.getElementById('reqs').textContent=d.server.requests;
|
||||
document.getElementById('heap').textContent=d.process.heap;
|
||||
document.getElementById('rss').textContent=d.process.rss;
|
||||
document.getElementById('plat').textContent=d.system.platform;
|
||||
document.getElementById('arch').textContent=d.system.arch;
|
||||
document.getElementById('node').textContent=d.system.node;
|
||||
}).catch(function(){})}
|
||||
fetchStats();setInterval(fetchStats,2000);
|
||||
</script></body></html>`;
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "notes-app",
|
||||
"version": "1.0.0",
|
||||
"main": "server.js",
|
||||
"scripts": { "start": "node server.js" },
|
||||
"dependencies": {}
|
||||
}
|
||||
34
app/src/main/assets/sample_projects/nodejs-koa-ar/server.js
Normal file
34
app/src/main/assets/sample_projects/nodejs-koa-ar/server.js
Normal file
@ -0,0 +1,34 @@
|
||||
|
||||
|
||||
|
||||
|
||||
const http = require('http'); const fs = require('fs'); const path = require('path');
|
||||
const PORT = process.env.PORT || 3000; const DATA_FILE = path.join(__dirname, 'notes.json');
|
||||
const loadNotes = () => { try { if (fs.existsSync(DATA_FILE)) return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); } catch (e) { } return [{ id: 1, title: 'مرحباً بتطبيق الملاحظات', content: '# مرحباً\n\nهذا تطبيق ملاحظات يدعم **Markdown**.\n\n## الميزات\n- إنشاء وتعديل\n- معاينة Markdown\n- بحث\n- حفظ محلي', createdAt: Date.now(), updatedAt: Date.now() }, { id: 2, title: 'صياغة Markdown', content: '# Markdown\n\n## التأكيد\n- **غامق**: `**نص**`\n- *مائل*: `*نص*`\n\n## القوائم\n- استخدم - للنقاط\n1. أرقام للترتيب', createdAt: Date.now() - 3600000, updatedAt: Date.now() - 3600000 }]; };
|
||||
const saveNotes = n => fs.writeFileSync(DATA_FILE, JSON.stringify(n, null, 2));
|
||||
let notes = loadNotes(), nextId = Math.max(...notes.map(n => n.id), 0) + 1;
|
||||
function parseBody(req) { return new Promise(r => { let b = ''; req.on('data', c => b += c); req.on('end', () => { try { r(JSON.parse(b)) } catch (e) { r({}) } }) }) }
|
||||
function sendJson(res, data, s = 200) { res.writeHead(s, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(data)) }
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
if (m === 'GET' && p === '/') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); return res.end(getPage()) }
|
||||
if (m === 'GET' && p === '/api/notes') { const q = url.searchParams.get('q'); let r = notes; if (q) { const ql = q.toLowerCase(); r = notes.filter(n => n.title.toLowerCase().includes(ql) || n.content.toLowerCase().includes(ql)) } return sendJson(res, { success: true, data: r.map(n => ({ ...n, preview: n.content.substring(0, 80) })) }) }
|
||||
if (m === 'POST' && p === '/api/notes') { const { title, content } = await parseBody(req); const n = { id: nextId++, title: title || 'بدون عنوان', content: content || '', createdAt: Date.now(), updatedAt: Date.now() }; notes.unshift(n); saveNotes(notes); return sendJson(res, { success: true, data: n }) }
|
||||
const nm = p.match(/^\/api\/notes\/(\d+)$/);
|
||||
if (m === 'GET' && nm) { const n = notes.find(x => x.id === parseInt(nm[1])); return n ? sendJson(res, { success: true, data: n }) : sendJson(res, { success: false }, 404) }
|
||||
if (m === 'PUT' && nm) { const n = notes.find(x => x.id === parseInt(nm[1])); if (!n) return sendJson(res, { success: false }, 404); const { title, content } = await parseBody(req); if (title !== undefined) n.title = title; if (content !== undefined) n.content = content; n.updatedAt = Date.now(); saveNotes(notes); return sendJson(res, { success: true, data: n }) }
|
||||
if (m === 'DELETE' && nm) { const i = notes.findIndex(x => x.id === parseInt(nm[1])); if (i === -1) return sendJson(res, { success: false }, 404); notes.splice(i, 1); saveNotes(notes); return sendJson(res, { success: true }) }
|
||||
sendJson(res, { error: 'Not Found' }, 404)
|
||||
});
|
||||
server.listen(PORT, '0.0.0.0', () => console.log('Notes on ' + PORT));
|
||||
|
||||
function getPage() {
|
||||
return `<!DOCTYPE html><html lang="ar" dir="rtl"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>ملاحظات</title>
|
||||
<style>*{margin:0;padding:0;box-sizing:border-box}:root{--bg:#0a0a1a;--card:rgba(255,255,255,0.04);--border:rgba(255,255,255,0.07);--text:#f0f0ff;--text2:rgba(240,240,255,0.55);--text3:rgba(240,240,255,0.3);--green:#10b981;--radius:14px}body{font-family:-apple-system,BlinkMacSystemFont,Tahoma,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;direction:rtl}body::before{content:'';position:fixed;top:-40%;left:-40%;width:180%;height:180%;background:radial-gradient(ellipse at 50% 30%,rgba(16,185,129,0.05) 0%,transparent 55%);pointer-events:none}.c{max-width:460px;margin:0 auto;padding:0 14px 32px;position:relative;z-index:1}.hdr{padding:20px 2px;display:flex;justify-content:space-between;align-items:center}.hdr h1{font-size:22px;font-weight:800;background:linear-gradient(135deg,#10b981,#34d399);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.hdr p{font-size:12px;color:var(--text2)}.new-btn{background:linear-gradient(135deg,#10b981,#059669);color:#fff;border:none;border-radius:10px;padding:8px 16px;font-size:12px;font-weight:700;cursor:pointer}.search{margin-bottom:14px}.search input{width:100%;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:10px 14px;color:var(--text);font-size:14px;outline:none}.search input:focus{border-color:var(--green)}.search input::placeholder{color:var(--text3)}.notes{display:flex;flex-direction:column;gap:10px}.note{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;cursor:pointer;position:relative;overflow:hidden}.note::before{content:'';position:absolute;top:0;right:0;width:3px;height:100%}.note:nth-child(1)::before{background:var(--green)}.note:nth-child(2)::before{background:#8b5cf6}.note:nth-child(3)::before{background:#f59e0b}.note-title{font-size:14px;font-weight:700;margin-bottom:4px;padding-left:30px}.note-preview{font-size:12px;color:var(--text2);line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.note-time{font-size:10px;color:var(--text3);margin-top:6px}.note-del{position:absolute;top:12px;left:12px;width:24px;height:24px;border-radius:6px;background:rgba(239,68,68,0.1);border:none;color:#ef4444;font-size:12px;cursor:pointer;display:flex;align-items:center;justify-content:center}.editor{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:14px;display:none}.editor input{width:100%;background:transparent;border:none;border-bottom:1px solid var(--border);padding:8px 0;color:var(--text);font-size:16px;font-weight:700;outline:none;margin-bottom:10px}.editor textarea{width:100%;background:transparent;border:1px solid var(--border);border-radius:8px;padding:10px;color:var(--text);font-size:13px;font-family:monospace;line-height:1.6;resize:none;outline:none;min-height:150px}.editor textarea:focus{border-color:var(--green)}.editor-actions{display:flex;gap:8px;margin-top:10px}.save-btn{background:var(--green);color:#fff;border:none;border-radius:8px;padding:8px 16px;font-size:12px;font-weight:700;cursor:pointer}.cancel-btn{background:var(--card);color:var(--text2);border:1px solid var(--border);border-radius:8px;padding:8px 16px;font-size:12px;cursor:pointer}.tabs{display:flex;gap:4px;margin-bottom:10px}.tab{padding:6px 14px;border-radius:8px;border:1px solid var(--border);color:var(--text2);font-size:12px;font-weight:600;cursor:pointer}.tab.active{background:rgba(16,185,129,0.1);color:var(--green)}.preview-area{margin-top:10px;padding:12px;background:rgba(0,0,0,0.2);border-radius:8px;font-size:13px;line-height:1.8;display:none}.empty{text-align:center;padding:40px;color:var(--text3)}.ft{text-align:center;padding:16px}.ft-b{font-size:10px;font-weight:600;color:var(--text3);padding:5px 12px;border-radius:20px;background:rgba(16,185,129,0.04)}@keyframes fadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.note{animation:fadeIn .4s ease backwards}</style></head><body>
|
||||
<div class="c"><div class="hdr"><div><h1>📝 ملاحظات Markdown</h1><p>Koa · Node.js</p></div><button class="new-btn" onclick="createNote()">+ جديد</button></div>
|
||||
<div class="search"><input type="text" id="searchInput" placeholder="بحث..." oninput="searchNotes()"></div>
|
||||
<div class="editor" id="editor"><input type="text" id="eTitle" placeholder="العنوان"><div class="tabs"><div class="tab active" onclick="switchTab('edit')">تحرير</div><div class="tab" onclick="switchTab('preview')">معاينة</div></div><textarea id="eContent" placeholder="اكتب شيئاً..." oninput="updatePreview()"></textarea><div class="preview-area" id="previewArea"></div><div class="editor-actions"><button class="save-btn" onclick="saveNote()">حفظ</button><button class="cancel-btn" onclick="closeEditor()">إلغاء</button></div></div>
|
||||
<div class="notes" id="noteList"></div>
|
||||
<div class="ft"><div class="ft-b">📝 Koa · Node.js · WebToApp</div></div></div>
|
||||
<script>var notes=[],currentId=null;function load(){var q=document.getElementById('searchInput').value;fetch('/api/notes'+(q?'?q='+encodeURIComponent(q):'')).then(function(r){return r.json()}).then(function(j){notes=j.data||[];renderList()})}function renderList(){var l=document.getElementById('noteList');if(notes.length===0){l.innerHTML='<div class="empty">📝 لا توجد ملاحظات</div>';return}l.innerHTML=notes.map(function(n,i){return'<div class="note" style="animation-delay:'+(i*.05)+'s" onclick="editNote('+n.id+')"><button class="note-del" onclick="event.stopPropagation();delNote('+n.id+')">×</button><div class="note-title">'+esc(n.title)+'</div><div class="note-preview">'+esc(n.preview||'')+'</div><div class="note-time">'+(new Date(n.updatedAt).toLocaleString())+'</div></div>'}).join('')}function esc(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML}function createNote(){fetch('/api/notes',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({title:'ملاحظة جديدة',content:''})}).then(function(r){return r.json()}).then(function(j){if(j.success){load();editNote(j.data.id)}})}function editNote(id){fetch('/api/notes/'+id).then(function(r){return r.json()}).then(function(j){if(j.success){currentId=j.data.id;document.getElementById('eTitle').value=j.data.title;document.getElementById('eContent').value=j.data.content;document.getElementById('editor').style.display='block';switchTab('edit')}})}function saveNote(){if(!currentId)return;fetch('/api/notes/'+currentId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({title:document.getElementById('eTitle').value,content:document.getElementById('eContent').value})}).then(function(){closeEditor();load()})}function closeEditor(){document.getElementById('editor').style.display='none';currentId=null}function delNote(id){fetch('/api/notes/'+id,{method:'DELETE'}).then(function(){if(currentId===id)closeEditor();load()})}function searchNotes(){load()}function switchTab(t){document.querySelectorAll('.tab').forEach(function(e){e.classList.remove('active')});if(t==='edit'){document.querySelector('.tab:first-child').classList.add('active');document.getElementById('eContent').style.display='block';document.getElementById('previewArea').style.display='none'}else{document.querySelector('.tab:last-child').classList.add('active');document.getElementById('eContent').style.display='none';document.getElementById('previewArea').style.display='block';updatePreview()}}function updatePreview(){var c=document.getElementById('eContent').value;document.getElementById('previewArea').innerHTML=simpleMd(c)}function simpleMd(s){return s.replace(/^### (.*$)/gm,'<h3>$1</h3>').replace(/^## (.*$)/gm,'<h2>$1</h2>').replace(/^# (.*$)/gm,'<h1>$1</h1>').replace(/\\*\\*(.*?)\\*\\*/g,'<strong>$1</strong>').replace(/\\*(.*?)\\*/g,'<em>$1</em>').replace(/\`(.*?)\`/g,'<code>$1</code>').replace(/^- (.*$)/gm,'<li>$1</li>').replace(/^> (.*$)/gm,'<blockquote>$1</blockquote>').replace(/\\n/g,'<br>')}load();</script></body></html>`;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "notes-app",
|
||||
"version": "1.0.0",
|
||||
"description": "Markdown Notes App - Zero dependency, uses Node.js built-in http module",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
38
app/src/main/assets/sample_projects/nodejs-koa-en/server.js
Normal file
38
app/src/main/assets/sample_projects/nodejs-koa-en/server.js
Normal file
@ -0,0 +1,38 @@
|
||||
|
||||
|
||||
|
||||
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const DATA_FILE = path.join(__dirname, 'notes.json');
|
||||
const loadNotes = () => { try { if (fs.existsSync(DATA_FILE)) return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); } catch (e) { } return [{ id: 1, title: 'Welcome to Notes', content: '# Welcome\n\nThis is a **Markdown** notes app.\n\n## Features\n- Create & edit notes\n- Markdown preview\n- Search\n- Local persistence', createdAt: Date.now(), updatedAt: Date.now() }, { id: 2, title: 'Markdown Syntax', content: '# Markdown\n\n## Emphasis\n- **Bold**: `**text**`\n- *Italic*: `*text*`\n\n## Lists\n- Use - for bullets\n1. Use numbers for ordered\n\n## Code\n`inline code`', createdAt: Date.now() - 3600000, updatedAt: Date.now() - 3600000 }]; };
|
||||
const saveNotes = n => fs.writeFileSync(DATA_FILE, JSON.stringify(n, null, 2));
|
||||
let notes = loadNotes(), nextId = Math.max(...notes.map(n => n.id), 0) + 1;
|
||||
function parseBody(req) { return new Promise(r => { let b = ''; req.on('data', c => b += c); req.on('end', () => { try { r(JSON.parse(b)) } catch (e) { r({}) } }) }) }
|
||||
function sendJson(res, data, s = 200) { res.writeHead(s, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(data)) }
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
if (m === 'GET' && p === '/') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); return res.end(getPage()) }
|
||||
if (m === 'GET' && p === '/api/notes') { const q = url.searchParams.get('q'); let r = notes; if (q) { const ql = q.toLowerCase(); r = notes.filter(n => n.title.toLowerCase().includes(ql) || n.content.toLowerCase().includes(ql)) } return sendJson(res, { success: true, data: r.map(n => ({ ...n, preview: n.content.substring(0, 80) })) }) }
|
||||
if (m === 'POST' && p === '/api/notes') { const { title, content } = await parseBody(req); const n = { id: nextId++, title: title || 'Untitled', content: content || '', createdAt: Date.now(), updatedAt: Date.now() }; notes.unshift(n); saveNotes(notes); return sendJson(res, { success: true, data: n }) }
|
||||
const nm = p.match(/^\/api\/notes\/(\d+)$/);
|
||||
if (m === 'GET' && nm) { const n = notes.find(x => x.id === parseInt(nm[1])); return n ? sendJson(res, { success: true, data: n }) : sendJson(res, { success: false }, 404) }
|
||||
if (m === 'PUT' && nm) { const n = notes.find(x => x.id === parseInt(nm[1])); if (!n) return sendJson(res, { success: false }, 404); const { title, content } = await parseBody(req); if (title !== undefined) n.title = title; if (content !== undefined) n.content = content; n.updatedAt = Date.now(); saveNotes(notes); return sendJson(res, { success: true, data: n }) }
|
||||
if (m === 'DELETE' && nm) { const i = notes.findIndex(x => x.id === parseInt(nm[1])); if (i === -1) return sendJson(res, { success: false }, 404); notes.splice(i, 1); saveNotes(notes); return sendJson(res, { success: true }) }
|
||||
sendJson(res, { error: 'Not Found' }, 404)
|
||||
});
|
||||
server.listen(PORT, '0.0.0.0', () => console.log('Notes on ' + PORT));
|
||||
|
||||
function getPage() {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>Notes</title>
|
||||
<style>*{margin:0;padding:0;box-sizing:border-box}:root{--bg:#0a0a1a;--card:rgba(255,255,255,0.04);--border:rgba(255,255,255,0.07);--text:#f0f0ff;--text2:rgba(240,240,255,0.55);--text3:rgba(240,240,255,0.3);--green:#10b981;--radius:14px}body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}body::before{content:'';position:fixed;top:-40%;left:-40%;width:180%;height:180%;background:radial-gradient(ellipse at 50% 30%,rgba(16,185,129,0.05) 0%,transparent 55%);pointer-events:none}.c{max-width:460px;margin:0 auto;padding:0 14px 32px;position:relative;z-index:1}.hdr{padding:20px 2px;display:flex;justify-content:space-between;align-items:center}.hdr h1{font-size:22px;font-weight:800;background:linear-gradient(135deg,#10b981,#34d399);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.hdr p{font-size:12px;color:var(--text2)}.new-btn{background:linear-gradient(135deg,#10b981,#059669);color:#fff;border:none;border-radius:10px;padding:8px 16px;font-size:12px;font-weight:700;cursor:pointer}.search{margin-bottom:14px}.search input{width:100%;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:10px 14px;color:var(--text);font-size:14px;outline:none}.search input:focus{border-color:var(--green)}.search input::placeholder{color:var(--text3)}.notes{display:flex;flex-direction:column;gap:10px}.note{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;cursor:pointer;position:relative;overflow:hidden}.note::before{content:'';position:absolute;top:0;left:0;width:3px;height:100%}.note:nth-child(1)::before{background:var(--green)}.note:nth-child(2)::before{background:#8b5cf6}.note:nth-child(3)::before{background:#f59e0b}.note:nth-child(4)::before{background:#06b6d4}.note-title{font-size:14px;font-weight:700;margin-bottom:4px;padding-right:30px}.note-preview{font-size:12px;color:var(--text2);line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.note-time{font-size:10px;color:var(--text3);margin-top:6px}.note-del{position:absolute;top:12px;right:12px;width:24px;height:24px;border-radius:6px;background:rgba(239,68,68,0.1);border:none;color:#ef4444;font-size:12px;cursor:pointer;display:flex;align-items:center;justify-content:center}.editor{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:14px;display:none}.editor input{width:100%;background:transparent;border:none;border-bottom:1px solid var(--border);padding:8px 0;color:var(--text);font-size:16px;font-weight:700;outline:none;margin-bottom:10px}.editor textarea{width:100%;background:transparent;border:1px solid var(--border);border-radius:8px;padding:10px;color:var(--text);font-size:13px;font-family:monospace;line-height:1.6;resize:none;outline:none;min-height:150px}.editor textarea:focus{border-color:var(--green)}.editor-actions{display:flex;gap:8px;margin-top:10px}.save-btn{background:var(--green);color:#fff;border:none;border-radius:8px;padding:8px 16px;font-size:12px;font-weight:700;cursor:pointer}.cancel-btn{background:var(--card);color:var(--text2);border:1px solid var(--border);border-radius:8px;padding:8px 16px;font-size:12px;cursor:pointer}.tabs{display:flex;gap:4px;margin-bottom:10px}.tab{padding:6px 14px;border-radius:8px;background:transparent;border:1px solid var(--border);color:var(--text2);font-size:12px;font-weight:600;cursor:pointer}.tab.active{background:rgba(16,185,129,0.1);color:var(--green)}.preview-area{margin-top:10px;padding:12px;background:rgba(0,0,0,0.2);border-radius:8px;font-size:13px;line-height:1.8;display:none}.preview-area h1,.preview-area h2{margin:10px 0 5px}.preview-area code{background:rgba(255,255,255,0.05);padding:2px 6px;border-radius:4px;font-family:monospace}.preview-area ul{margin:5px 0 5px 20px}.preview-area blockquote{border-left:3px solid var(--green);padding-left:12px;color:var(--text2)}.empty{text-align:center;padding:40px;color:var(--text3)}.ft{text-align:center;padding:16px}.ft-b{font-size:10px;font-weight:600;color:var(--text3);padding:5px 12px;border-radius:20px;background:rgba(16,185,129,0.04)}@keyframes fadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.note{animation:fadeIn .4s ease backwards}</style></head><body>
|
||||
<div class="c"><div class="hdr"><div><h1>📝 Markdown Notes</h1><p>Koa · Node.js</p></div><button class="new-btn" onclick="createNote()">+ New</button></div>
|
||||
<div class="search"><input type="text" id="searchInput" placeholder="Search notes..." oninput="searchNotes()"></div>
|
||||
<div class="editor" id="editor"><input type="text" id="eTitle" placeholder="Title"><div class="tabs"><div class="tab active" onclick="switchTab('edit')">Edit</div><div class="tab" onclick="switchTab('preview')">Preview</div></div><textarea id="eContent" placeholder="Write something..." oninput="updatePreview()"></textarea><div class="preview-area" id="previewArea"></div><div class="editor-actions"><button class="save-btn" onclick="saveNote()">Save</button><button class="cancel-btn" onclick="closeEditor()">Cancel</button></div></div>
|
||||
<div class="notes" id="noteList"></div>
|
||||
<div class="ft"><div class="ft-b">📝 Koa · Node.js · WebToApp</div></div></div>
|
||||
<script>var notes=[],currentId=null;function load(){var q=document.getElementById('searchInput').value;fetch('/api/notes'+(q?'?q='+encodeURIComponent(q):'')).then(function(r){return r.json()}).then(function(j){notes=j.data||[];renderList()})}function renderList(){var l=document.getElementById('noteList');if(notes.length===0){l.innerHTML='<div class="empty">📝 No notes yet</div>';return}l.innerHTML=notes.map(function(n,i){return'<div class="note" style="animation-delay:'+(i*.05)+'s" onclick="editNote('+n.id+')"><button class="note-del" onclick="event.stopPropagation();delNote('+n.id+')">×</button><div class="note-title">'+esc(n.title)+'</div><div class="note-preview">'+esc(n.preview||'')+'</div><div class="note-time">'+(new Date(n.updatedAt).toLocaleString())+'</div></div>'}).join('')}function esc(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML}function createNote(){fetch('/api/notes',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({title:'New Note',content:''})}).then(function(r){return r.json()}).then(function(j){if(j.success){load();editNote(j.data.id)}})}function editNote(id){fetch('/api/notes/'+id).then(function(r){return r.json()}).then(function(j){if(j.success){currentId=j.data.id;document.getElementById('eTitle').value=j.data.title;document.getElementById('eContent').value=j.data.content;document.getElementById('editor').style.display='block';switchTab('edit')}})}function saveNote(){if(!currentId)return;fetch('/api/notes/'+currentId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({title:document.getElementById('eTitle').value,content:document.getElementById('eContent').value})}).then(function(){closeEditor();load()})}function closeEditor(){document.getElementById('editor').style.display='none';currentId=null}function delNote(id){fetch('/api/notes/'+id,{method:'DELETE'}).then(function(){if(currentId===id)closeEditor();load()})}function searchNotes(){load()}function switchTab(t){document.querySelectorAll('.tab').forEach(function(e){e.classList.remove('active')});if(t==='edit'){document.querySelector('.tab:first-child').classList.add('active');document.getElementById('eContent').style.display='block';document.getElementById('previewArea').style.display='none'}else{document.querySelector('.tab:last-child').classList.add('active');document.getElementById('eContent').style.display='none';document.getElementById('previewArea').style.display='block';updatePreview()}}function updatePreview(){var c=document.getElementById('eContent').value;document.getElementById('previewArea').innerHTML=simpleMd(c)}function simpleMd(s){return s.replace(/^### (.*$)/gm,'<h3>$1</h3>').replace(/^## (.*$)/gm,'<h2>$1</h2>').replace(/^# (.*$)/gm,'<h1>$1</h1>').replace(/\\*\\*(.*?)\\*\\*/g,'<strong>$1</strong>').replace(/\\*(.*?)\\*/g,'<em>$1</em>').replace(/\`(.*?)\`/g,'<code>$1</code>').replace(/^- (.*$)/gm,'<li>$1</li>').replace(/^> (.*$)/gm,'<blockquote>$1</blockquote>').replace(/\\n/g,'<br>')}load();</script></body></html>`;
|
||||
}
|
||||
12
app/src/main/assets/sample_projects/nodejs-koa/package.json
Normal file
12
app/src/main/assets/sample_projects/nodejs-koa/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "notes-app",
|
||||
"version": "1.0.0",
|
||||
"description": "Markdown 笔记应用 - 零依赖,使用 Node.js 内置 http 模块",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {},
|
||||
"keywords": ["notes", "markdown", "nodejs", "zero-dependency"],
|
||||
"author": "WebToApp"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user