Standalone mobile test (#9513)

This commit is contained in:
Elian Doran
2026-04-20 23:10:33 +03:00
committed by GitHub
77 changed files with 1647 additions and 132 deletions

View File

@@ -2,6 +2,7 @@ on:
push:
branches:
- "main"
- "standalone"
- "feature/update**"
- "feature/server_esm**"
paths-ignore:

57
.github/workflows/mobile.yml vendored Normal file
View File

@@ -0,0 +1,57 @@
name: Mobile
on:
push:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build_android:
name: Build Android APK
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: "pnpm"
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v5
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Update build info
run: pnpm run chore:update-build-info
- name: Build client-standalone (webDir for Capacitor)
run: pnpm --filter @triliumnext/mobile build
- name: Sync Capacitor Android project
run: pnpm --filter @triliumnext/mobile exec cap sync android
- name: Assemble debug APK
working-directory: apps/mobile/android
run: ./gradlew assembleDebug --no-daemon
- name: Upload APK
uses: actions/upload-artifact@v7
with:
name: trilium-mobile-debug-apk
path: apps/mobile/android/app/build/outputs/apk/debug/*.apk
retention-days: 14

View File

@@ -301,9 +301,14 @@ export default class BrowserSqlProvider implements DatabaseProvider {
* Must be called after `initWasm()` and before `loadFromSahPool()`.
* This is async because it acquires OPFS file handles.
*
* Unlike the legacy OPFS VFS, SAHPool does **not** require SharedArrayBuffer
* or COOP/COEP headers — it only needs OPFS itself (a Worker context with
* `navigator.storage.getDirectory`). This makes it usable in Capacitor's
* Android WebView, which doesn't support cross-origin isolation.
*
* @param options.directory - OPFS directory for the pool (default: auto-derived from VFS name)
* @param options.initialCapacity - Minimum number of file slots (default: 6)
* @throws Error if the environment doesn't support SAHPool (no OPFS, no Worker, no COOP/COEP)
* @throws Error if the environment doesn't support OPFS (no Worker, or no OPFS API)
*/
async installSahPool(options: { directory?: string; initialCapacity?: number } = {}): Promise<void> {
this.ensureSqlite3();
@@ -508,11 +513,11 @@ export default class BrowserSqlProvider implements DatabaseProvider {
loadFromFile(_path: string, _isReadOnly: boolean): void {
// Browser environment doesn't have direct file system access.
// Use OPFS for persistent storage.
// Use SAHPool or OPFS for persistent storage.
throw new Error(
"loadFromFile is not supported in browser environment. " +
"Use loadFromMemory() for temporary databases, loadFromBuffer() to load from data, " +
"or loadFromOpfs() for persistent storage."
"loadFromSahPool() (preferred) or loadFromOpfs() for persistent storage."
);
}
@@ -728,7 +733,10 @@ export default class BrowserSqlProvider implements DatabaseProvider {
private ensureDb(): void {
this.ensureSqlite3();
if (!this.db) {
throw new Error("Database not opened. Call loadFromMemory(), loadFromBuffer(), or loadFromOpfs() first.");
throw new Error(
"Database not opened. Call loadFromMemory(), loadFromBuffer(), " +
"loadFromSahPool(), or loadFromOpfs() first."
);
}
}
}

View File

@@ -342,13 +342,16 @@ async function initialize(): Promise<void> {
console.log("[Worker] SAHPool available, loading persistent database (WAL mode)...");
sqlProvider!.loadFromSahPool(dbName);
} else if (sqlProvider!.isOpfsAvailable()) {
// Fall back to legacy OPFS VFS (no WAL, slower writes)
console.warn("[Worker] Using legacy OPFS VFS (no WAL mode). Consider enabling COOP/COEP headers for SAHPool.");
// Fall back to legacy OPFS VFS (no WAL, slower writes).
// This only kicks in if SAHPool installation failed for some
// reason but SharedArrayBuffer + legacy OPFS are both available.
console.warn("[Worker] SAHPool unavailable; using legacy OPFS VFS (no WAL mode).");
sqlProvider!.loadFromOpfs(dbName);
} else {
// Fall back to in-memory database (non-persistent)
// Fall back to in-memory database (non-persistent).
// SAHPool only needs a Worker + OPFS API, so reaching this
// branch means the environment lacks OPFS entirely.
console.warn("[Worker] OPFS not available, using in-memory database (data will not persist)");
console.warn("[Worker] To enable persistence, ensure COOP/COEP headers are set by the server");
sqlProvider!.loadFromMemory();
}

View File

@@ -24,6 +24,7 @@ import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import QuickSearchWidget from "../widgets/quick_search.js";
import { isMobileApp } from "../services/utils";
import ScrollPadding from "../widgets/scroll_padding";
import SearchResult from "../widgets/search_result.jsx";
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
@@ -65,7 +66,8 @@ export default class MobileLayout {
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />)
.child(<NoteBadges />)
.optChild(glob.isStandalone, <StandaloneWarningBar />)
.optChild(isMobileApp(), <StandaloneWarningBar variant="mobile" />)
.optChild(glob.isStandalone && !isMobileApp(), <StandaloneWarningBar />)
.child(<MobileDetailMenu />)
)
.child(

View File

@@ -149,6 +149,14 @@ export function isPWA() {
);
}
/**
* Returns `true` when running inside the native Capacitor mobile app wrapper.
* PWAs and regular browsers return `false`.
*/
export function isMobileApp() {
return !!window.Capacitor?.isNativePlatform?.();
}
export function isMac() {
return navigator.platform.indexOf("Mac") > -1;
}

View File

@@ -52,6 +52,10 @@ body.setup {
justify-content: center;
gap: 1rem;
body.desktop & {
gap: 0.75rem;
}
.setup-option-card {
padding: 1.5em;
cursor: pointer;
@@ -78,8 +82,12 @@ body.setup {
}
h3 {
font-size: 1.5em;
font-size: 1.15em;
font-weight: normal;
body.desktop & {
font-size: 1.5em;
}
}
p:last-of-type {
@@ -94,15 +102,23 @@ body.setup {
display: flex;
flex-direction: column;
height: 100%;
padding: 2em;
padding-top: calc(2em + env(safe-area-inset-top));
padding-bottom: calc(2em + env(safe-area-inset-bottom));
padding-left: calc(2em + env(safe-area-inset-left));
padding-right: calc(2em + env(safe-area-inset-right));
overflow: auto;
>.back-button {
position: absolute;
top: 2em;
inset-inline-start: 2em;
top: calc(1em + env(safe-area-inset-top));
inset-inline-start: 1em;
color: var(--muted-text-color);
body.desktop & {
inset-inline-start: 2em;
top: 2em;
}
.tn-icon {
margin-inline-end: 0.4em;
}
@@ -116,8 +132,11 @@ body.setup {
flex: 1;
display: flex;
flex-direction: column;
padding-top: 1em;
min-height: 0;
padding-top: 2em;
body.desktop & {
padding-top: 1em;
}
}
&.contentless {
@@ -126,13 +145,20 @@ body.setup {
}
>footer {
background: var(--main-background-color);
position: sticky;
bottom: -2rem;
left: 0;
right: 0;
display: flex;
justify-content: flex-end;
gap: 0.5rem;
border-top: 1px solid var(--main-border-color);
padding-top: 1rem;
margin-inline: -2em;
padding-inline: 2em;
padding-bottom: 2rem;
margin-inline: -2rem;
margin-bottom: -2rem;
padding-inline: 2rem;
}
>.page-error {
@@ -158,9 +184,13 @@ body.setup {
display: flex;
flex-direction: column;
gap: 1rem;
width: 80%;
width: 100%;
margin-inline: auto;
body.desktop & {
width: 80%;
}
.form-group {
margin-bottom: 0;
}
@@ -187,6 +217,11 @@ body.setup {
justify-content: center;
margin-top: 1.5em;
margin-bottom: 1.5rem;
padding-block: 2em;
body.desktop & {
padding-block: 1em;
}
.tn-icon {
font-size: 3em;
@@ -223,13 +258,27 @@ body.setup {
text-align: center;
color: var(--muted-text-color);
opacity: 0.6;
margin-block: 1rem;
}
.illustration-logo {
width: 96px;
height: 96px;
--size: 128px;
width: var(--size);
height: var(--size);
margin: auto;
body.desktop & {
--size: 96px;
}
}
.illustration-icon,
.illustration-logo {
margin-top: 3rem;
margin-bottom: 1rem;
body.desktop & {
margin-top: 1rem;
}
}
h1 {
@@ -328,21 +377,30 @@ body[dir=rtl] .slide-in-backward {
}
.page.select-language {
.dropdownWrapper {
padding-bottom: 2em;
width: 80%;
margin: auto;
main {
min-height: 0;
}
.dropdownWrapper,
.dropdown,
.dropdown-menu {
.tn-card {
width: 100%;
margin: 0 auto 2em;
height: 100%;
box-sizing: border-box;
min-height: 0;
overflow: hidden;
body.desktop & {
width: 80%;
}
}
.tn-card-body {
height: 100%;
}
.dropdown-menu {
box-sizing: border-box;
.tn-card-section {
overflow: auto;
padding: 0.5em 0;
}
}

View File

@@ -15,7 +15,7 @@ import Admonition from "./widgets/react/Admonition";
import Button from "./widgets/react/Button";
import { Card, CardFrame, CardSection } from "./widgets/react/Card";
import FormGroup from "./widgets/react/FormGroup";
import FormList, { FormListItem } from "./widgets/react/FormList";
import { FormListItem } from "./widgets/react/FormList";
import FormTextBox from "./widgets/react/FormTextBox";
import Icon from "./widgets/react/Icon";
@@ -24,7 +24,7 @@ async function main() {
const bodyWrapper = document.createElement("div");
bodyWrapper.classList.add("setup-outer-wrapper");
document.body.classList.add("setup");
document.body.classList.add("setup", window.glob.device || "desktop");
if (isElectron()) {
document.body.classList.add("electron", `platform-${window.process.platform}`, "background-effects");
}
@@ -101,16 +101,24 @@ function SelectLanguage({ setState }: { setState: (state: State) => void }) {
illustration={<Icon icon="bx bx-globe" className="illustration-icon" />}
footer={<Button text={t("setup.continue")} kind="primary" onClick={() => setState("firstOptions")} />}
>
<FormList onSelect={async (id) => {
await i18n.changeLanguage(id);
setCurrentLocale(id);
const locale = LOCALES.find(l => l.id === id);
document.body.dir = locale?.rtl ? "rtl" : "ltr";
}}>
{filteredLocales.map(locale => (
<FormListItem key={locale.id} value={locale.id} active={locale.id === currentLocale}>{locale.name}</FormListItem>
))}
</FormList>
<Card>
<CardSection>
{filteredLocales.map(locale => (
<FormListItem
key={locale.id}
value={locale.id}
active={locale.id === currentLocale}
onClick={async () => {
await i18n.changeLanguage(locale.id);
setCurrentLocale(locale.id);
document.body.dir = locale.rtl ? "rtl" : "ltr";
}}
>
{locale.name}
</FormListItem>
))}
</CardSection>
</Card>
</SetupPage>
);
}

View File

@@ -1756,6 +1756,10 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
max-height: unset;
max-width: unset;
.modal-header {
margin-top: env(safe-area-inset-top);
}
.modal-content {
border-radius: 0;
border: 0;

View File

@@ -3,6 +3,10 @@
"badge_label": "Standalone",
"warning_tooltip": "You are running Trilium in standalone mode. Some features are not available, and you may experience issues or data loss. Use the desktop application or self-hosted server for the best experience."
},
"mobile": {
"badge_label": "Mobile",
"warning_tooltip": "You are running Trilium in mobile mode. Some features are not available. Use the desktop application or desktop layout for the best experience."
},
"about": {
"version_label": "Version:",
"version": "{{appVersion}} (database: {{dbVersion}}, sync protocol: {{syncVersion}})",

View File

@@ -51,6 +51,11 @@ declare global {
_noteReady?: PrintReport;
EXCALIDRAW_ASSET_PATH?: string;
Capacitor?: {
isNativePlatform?: () => boolean;
getPlatform?: () => string;
};
}
interface WindowEventMap {

View File

@@ -1,21 +1,23 @@
import Modal from "../react/Modal.js";
import "./about.css";
import type { AppInfo, Contributor, ContributorList } from "@triliumnext/commons";
import clsx from "clsx";
import type { ComponentChildren } from "preact";
import { memo,useMemo } from "preact/compat";
import { useCallback, useRef,useState } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
import type React from "react";
import { Trans } from "react-i18next";
import contributors from "../../../../../contributors.json";
import { t } from "../../services/i18n.js";
import { formatDateTime } from "../../utils/formatters.js";
import openService from "../../services/open.js";
import server from "../../services/server.js";
import utils from "../../services/utils.js";
import openService from "../../services/open.js";
import { useState, useCallback, useRef } from "preact/hooks";
import type { AppInfo, Contributor, ContributorList } from "@triliumnext/commons";
import { formatDateTime } from "../../utils/formatters.js";
import { useTooltip, useTriliumEvent } from "../react/hooks.jsx";
import Modal from "../react/Modal.js";
import { PropertySheet, PropertySheetItem } from "../react/PropertySheet.js";
import "./about.css";
import { Trans } from "react-i18next";
import type React from "react";
import contributors from "../../../../../contributors.json";
import { Fragment } from "preact/jsx-runtime";
import type { ComponentChildren } from "preact";
import { useMemo, memo } from "preact/compat";
import clsx from "clsx";
export default function AboutDialog() {
const [appInfo, setAppInfo] = useState<AppInfo | null>(null);
@@ -55,17 +57,17 @@ export default function AboutDialog() {
setAltIcon(null);
}
}
}
};
};
/* Cache the contributor list to prevent its rerendering.
* When the icon changes, it triggers a rerender of the dialog. If this happens while an
* element with a tooltip is hovered, its tooltip will break. */
const CachedContributors = useMemo(() => memo(function CachedContributors() {
return <Contributors
const CachedContributors = useMemo(() => memo(() => {
return <Contributors
data={contributors as ContributorList}
onHover={createContributorHoverHandler()}
/>
/>;
}), []);
return (
@@ -76,8 +78,8 @@ export default function AboutDialog() {
show={isShown}
onHidden={() => setIsShown(false)}
>
<div className="about-dialog-content">
<div className="about-dialog-content">
<div className={"icon"} data-icon={altIcon ?? icon} />
<h2>Trilium Notes {isNightly && <span className="channel-name">Nightly</span>}</h2>
<a className="tn-link" href="https://triliumnotes.org/" target="_blank" rel="noopener noreferrer">
@@ -112,23 +114,25 @@ export default function AboutDialog() {
</a>
</PropertySheetItem>
<PropertySheetItem label={t("about.data_directory")}>
<div style={{wordBreak: "break-all"}}>
{appInfo?.dataDirectory && (<DirectoryLink directory={appInfo.dataDirectory} />)}
</div>
</PropertySheetItem>
{appInfo?.dataDirectory && (
<PropertySheetItem label={t("about.data_directory")}>
<div style={{wordBreak: "break-all"}}>
<DirectoryLink directory={appInfo.dataDirectory} />
</div>
</PropertySheetItem>
)}
</PropertySheet>
</div>
</div>
<footer>
<FooterLink
<footer>
<FooterLink
text="GitHub"
url="https://github.com/TriliumNext/Trilium"
tooltip={t("about.github_tooltip")}>
<i className='bx bxl-github'></i>
<i className='bx bxl-github' />
</FooterLink>
<FooterLink
text="AGPL 3.0"
url="https://docs.triliumnotes.org/user-guide/misc/license"
@@ -144,9 +148,9 @@ export default function AboutDialog() {
tooltip={t("about.donate_tooltip")}
className="donate-link">
<i className='bx bx-heart' ></i>
<i className='bx bx-heart' />
</FooterLink>
</footer>
</footer>
</Modal>
);
}
@@ -160,19 +164,18 @@ function RevisionLink({appInfo}: {appInfo: AppInfo | null}) {
}
function FooterLink(props: {children: ComponentChildren, text: string, url: string, tooltip: string, className?: string}) {
const linkRef = useRef<HTMLAnchorElement>(null);
useTooltip(linkRef, {
title: props.tooltip,
delay: 250,
placement: "bottom"
})
});
return <a ref={linkRef} href={props.url} className={props.className} target="_blank" rel="noopener noreferrer" draggable={false}>
{props.children}
{props.text}
</a>
</a>;
}
type HoverCallback = (contributor: Contributor, isHovering: boolean, part: "name" | "role") => void;
@@ -181,10 +184,10 @@ function Contributors({data, onHover}: {data: ContributorList, onHover?: HoverCa
return data.contributors.map((c, index, array) => {
return <Fragment key={c.name}>
<ContributorListItem data={c} onHover={onHover} />
{/* Add a comma between items */}
{(index < array.length - 1) ? ", " : ". "}
</Fragment>
</Fragment>;
});
}
@@ -208,7 +211,7 @@ function ContributorListItem({data, onHover}: {data: Contributor, onHover?: Hove
rel="noopener noreferrer"
onMouseEnter={() => onHover?.(data, true, "name")}
onMouseLeave={() => onHover?.(data, false, "name")}>
{data.fullName ?? data.name}
</a>
@@ -216,10 +219,10 @@ function ContributorListItem({data, onHover}: {data: Contributor, onHover?: Hove
ref={roleRef}
onMouseEnter={() => onHover?.(data, true, "role")}
onMouseLeave={() => onHover?.(data, false, "role")}>
(<span className="contributor-role">{roleString}</span>)
</span>}
</>
</span>}
</>;
}
function DirectoryLink({ directory }: { directory: string}) {
@@ -229,8 +232,7 @@ function DirectoryLink({ directory }: { directory: string}) {
openService.openDirectory(directory);
};
return <a className="tn-link selectable-text" href="#" onClick={onClick}>{directory}</a>
} else {
return <span className="selectable-text">{directory}</span>;
return <a className="tn-link selectable-text" href="#" onClick={onClick}>{directory}</a>;
}
}
return <span className="selectable-text">{directory}</span>;
}

View File

@@ -3,12 +3,18 @@ import { t } from "../../services/i18n";
import { useNoteContext, useTooltip } from "../react/hooks";
import "./StandaloneWarningBar.css";
export default function StandaloneWarningBar() {
type WarningBarVariant = "standalone" | "mobile";
interface WarningBarProps {
variant?: WarningBarVariant;
}
export default function StandaloneWarningBar({ variant = "standalone" }: WarningBarProps) {
const { noteContext } = useNoteContext();
const badgeRef = useRef<HTMLDivElement>(null);
useTooltip(badgeRef, {
title: t("standalone.warning_tooltip"),
title: t(`${variant}.warning_tooltip`),
placement: "top",
delay: 200
});
@@ -21,7 +27,7 @@ export default function StandaloneWarningBar() {
return (
<div ref={badgeRef} className="standalone-badge">
<span className="bx bx-error-circle" />
<span className="standalone-badge-text">{t("standalone.badge_label")}</span>
<span className="standalone-badge-text">{t(`${variant}.badge_label`)}</span>
</div>
);
}

7
apps/mobile/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
.idea/
node_modules/
.vscode/
*.map
.DS_Store
.sourcemaps
dist/

29
apps/mobile/README.md Normal file
View File

@@ -0,0 +1,29 @@
# @triliumnext/mobile
Capacitor shell that wraps the [`@triliumnext/client-standalone`](../client-standalone/) PWA build as a native mobile app. This package does not ship its own web assets — `webDir` in [capacitor.config.json](./capacitor.config.json) points directly at `../client-standalone/dist`.
## Prerequisites
- Android SDK + an emulator or attached device (set up `ANDROID_HOME` / `ANDROID_SDK_ROOT`).
- JDK 17+.
- The monorepo installed: `corepack enable && pnpm install` at the repo root.
## First-time setup
```bash
# 1. Build the standalone web app into apps/client-standalone/dist
pnpm --filter @triliumnext/mobile build
# 2. Generate the native Android project (one-off — commits as apps/mobile/android/)
pnpm --filter @triliumnext/mobile exec cap add android
```
## Everyday loop
```bash
pnpm --filter @triliumnext/mobile build # rebuild standalone dist
pnpm --filter @triliumnext/mobile sync # copy dist into android/
pnpm --filter @triliumnext/mobile run:android # launch on emulator/device
# or
pnpm --filter @triliumnext/mobile open:android # open Android Studio
```

101
apps/mobile/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml

2
apps/mobile/android/app/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep

View File

@@ -0,0 +1,54 @@
apply plugin: 'com.android.application'
android {
namespace = "org.triliumnotes.trilium"
compileSdk = rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "org.triliumnotes.trilium"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

View File

@@ -0,0 +1,19 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-splash-screen')
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<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"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@@ -0,0 +1,5 @@
package org.triliumnotes.trilium;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FAFAFA</color>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">Trilium Notes</string>
<string name="title_activity_main">Trilium Notes</string>
<string name="package_name">org.triliumnotes.trilium</string>
<string name="custom_url_scheme">org.triliumnotes.trilium</string>
</resources>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

View File

@@ -0,0 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
classpath 'com.google.gms:google-services:4.4.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,6 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../../../node_modules/@capacitor/android/capacitor')
include ':capacitor-splash-screen'
project(':capacitor-splash-screen').projectDir = new File('../../../node_modules/@capacitor/splash-screen/android')

View File

@@ -0,0 +1,22 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
apps/mobile/android/gradlew vendored Executable file
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
apps/mobile/android/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,5 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'

View File

@@ -0,0 +1,16 @@
ext {
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
androidxActivityVersion = '1.11.0'
androidxAppCompatVersion = '1.7.1'
androidxCoordinatorLayoutVersion = '1.3.0'
androidxCoreVersion = '1.17.0'
androidxFragmentVersion = '1.8.9'
coreSplashScreenVersion = '1.2.0'
androidxWebkitVersion = '1.14.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.3.0'
androidxEspressoCoreVersion = '3.7.0'
cordovaAndroidVersion = '14.0.1'
}

View File

@@ -0,0 +1,16 @@
{
"appId": "org.triliumnotes.trilium",
"appName": "Trilium Notes",
"webDir": "../client-standalone/dist",
"server": {
"androidScheme": "https",
"hostname": "localhost"
},
"plugins": {
"SplashScreen": {
"launchAutoHide": true,
"launchShowDuration": 2000,
"launchFadeOutDuration": 300
}
}
}

22
apps/mobile/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "@triliumnext/mobile",
"version": "0.102.2",
"description": "Capacitor-based mobile app for TriliumNext, built on the standalone client.",
"private": true,
"license": "AGPL-3.0-only",
"type": "module",
"scripts": {
"build": "pnpm --filter @triliumnext/client-standalone build",
"sync": "pnpm build && cap sync",
"open:android": "cap open android",
"run:android": "cap run android"
},
"dependencies": {
"@capacitor/android": "8.3.1",
"@capacitor/core": "8.3.1",
"@capacitor/splash-screen": "8.0.1"
},
"devDependencies": {
"@capacitor/cli": "8.3.1"
}
}

View File

@@ -26,6 +26,12 @@ import utils, { getResourceDir, isDev } from "./services/utils.js";
// Allow serving assets even if the installation path contains a hidden (dot-prefixed) directory.
const STATIC_OPTIONS: serveStatic.ServeStaticOptions = { dotfiles: "allow" };
// Capacitor WebView origins for the mobile app — baked in so the mobile client
// can sync against any Trilium server without per-server CORS configuration.
const MOBILE_APP_ORIGINS = ["https://localhost", "capacitor://localhost"];
const DEFAULT_CORS_METHODS = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
const DEFAULT_CORS_HEADERS = "Content-Type, Authorization, trilium-cred, pageCount, pageIndex, requestId";
export default async function buildApp() {
const app = express();
@@ -38,25 +44,9 @@ export default async function buildApp() {
app.engine("ejs", (filePath, options, callback) => ejs.renderFile(filePath, options, callback));
app.set("view engine", "ejs");
app.use((req, res, next) => {
// set CORS headers
if (config["Network"]["corsAllowOrigin"]) {
res.header("Access-Control-Allow-Origin", config["Network"]["corsAllowOrigin"]);
res.header("Access-Control-Allow-Credentials", "true");
}
if (config["Network"]["corsAllowMethods"]) {
res.header("Access-Control-Allow-Methods", config["Network"]["corsAllowMethods"]);
}
if (config["Network"]["corsAllowHeaders"]) {
res.header("Access-Control-Allow-Headers", config["Network"]["corsAllowHeaders"]);
}
// Handle preflight OPTIONS requests
if (req.method === "OPTIONS" && config["Network"]["corsAllowOrigin"]) {
res.sendStatus(204);
return;
}
setupCors(app);
app.use((_req, res, next) => {
res.locals.t = t;
return next();
});
@@ -133,3 +123,28 @@ export default async function buildApp() {
return app;
}
function setupCors(app: express.Express) {
const configuredOrigins = (config["Network"]["corsAllowOrigin"] || "")
.split(",")
.map(o => o.trim())
.filter(Boolean);
const allowedOrigins = new Set([...MOBILE_APP_ORIGINS, ...configuredOrigins]);
app.use((req, res, next) => {
const requestOrigin = req.get("origin");
if (requestOrigin && allowedOrigins.has(requestOrigin)) {
res.header("Access-Control-Allow-Origin", requestOrigin);
res.header("Access-Control-Allow-Credentials", "true");
res.header("Vary", "Origin");
res.header("Access-Control-Allow-Methods", config["Network"]["corsAllowMethods"] || DEFAULT_CORS_METHODS);
res.header("Access-Control-Allow-Headers", config["Network"]["corsAllowHeaders"] || DEFAULT_CORS_HEADERS);
if (req.method === "OPTIONS") {
res.sendStatus(204);
return;
}
}
return next();
});
}

View File

@@ -19,6 +19,8 @@
"desktop:build": "pnpm run --filter desktop build",
"desktop:start-prod": "pnpm run --filter desktop start-prod",
"standalone:start": "pnpm run --filter client-standalone dev",
"standalone:build": "pnpm run --filter client-standalone build",
"mobile:sync": "pnpm run --filter mobile sync",
"desktop:start-prod-no-dir": "pnpm run --filter desktop start-prod-no-dir",
"edit-docs:edit-docs": "pnpm run --filter edit-docs edit-docs",
"edit-docs:build": "pnpm run --filter edit-docs build",

308
pnpm-lock.yaml generated
View File

@@ -808,6 +808,22 @@ importers:
specifier: 2.1.2
version: 2.1.2
apps/mobile:
dependencies:
'@capacitor/android':
specifier: 8.3.1
version: 8.3.1(@capacitor/core@8.3.1)
'@capacitor/core':
specifier: 8.3.1
version: 8.3.1
'@capacitor/splash-screen':
specifier: 8.0.1
version: 8.0.1(@capacitor/core@8.3.1)
devDependencies:
'@capacitor/cli':
specifier: 8.3.1
version: 8.3.1
apps/server:
dependencies:
'@ai-sdk/anthropic':
@@ -1970,6 +1986,24 @@ packages:
'@cacheable/utils@2.3.3':
resolution: {integrity: sha512-JsXDL70gQ+1Vc2W/KUFfkAJzgb4puKwwKehNLuB+HrNKWf91O736kGfxn4KujXCCSuh6mRRL4XEB0PkAFjWS0A==}
'@capacitor/android@8.3.1':
resolution: {integrity: sha512-hjskIG8YcBEh3X4yaTXvE9gcqpdcxunTgFruSKnuPxtMxAUzEK4Oq25x0Z1g3cz+MQPc+lRG09R7Ovc+ydKsNw==}
peerDependencies:
'@capacitor/core': ^8.3.0
'@capacitor/cli@8.3.1':
resolution: {integrity: sha512-1sPGW4THTDfR6YjXwZ0jM7oAfAtciPOHN00qs/3sNAQx1kKrrEYSfDPwCm1/xlAgi0OeL69SiRfw314Ans+1sw==}
engines: {node: '>=22.0.0'}
hasBin: true
'@capacitor/core@8.3.1':
resolution: {integrity: sha512-UF8ItlHguU1Z6GXfPTeT2gakf+ctNI8pAS1kwSBQlsJMlfD4OPoto/SmKnOxKCQvnF4WRcdWeg6C0zREUNaAQg==}
'@capacitor/splash-screen@8.0.1':
resolution: {integrity: sha512-c/ew/Z3eA7z8l06WoRAtzVF16VwYYrExmHmfGq1Cg675pVzaC/yuucB8/1xG1vhEfnW4fZ1KhSf/kzR1RiVYgg==}
peerDependencies:
'@capacitor/core': '>=8.0.0'
'@catppuccin/codemirror@1.0.3':
resolution: {integrity: sha512-P1ZCj4DZVLqr/TNz28m3aaF2Elrikpb8OOnzN0Vyf95Un4pTWTkCSvhbskbnJbnNJ87Rfvt3fXoaUj4o55X30Q==}
@@ -2615,9 +2649,6 @@ packages:
'@emnapi/core@1.9.2':
resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==}
'@emnapi/runtime@1.9.0':
resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==}
'@emnapi/runtime@1.9.1':
resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
@@ -3776,6 +3807,38 @@ packages:
'@types/node':
optional: true
'@ionic/cli-framework-output@2.2.8':
resolution: {integrity: sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==}
engines: {node: '>=16.0.0'}
'@ionic/utils-array@2.1.6':
resolution: {integrity: sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==}
engines: {node: '>=16.0.0'}
'@ionic/utils-fs@3.1.7':
resolution: {integrity: sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==}
engines: {node: '>=16.0.0'}
'@ionic/utils-object@2.1.6':
resolution: {integrity: sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==}
engines: {node: '>=16.0.0'}
'@ionic/utils-process@2.1.12':
resolution: {integrity: sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==}
engines: {node: '>=16.0.0'}
'@ionic/utils-stream@3.1.7':
resolution: {integrity: sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==}
engines: {node: '>=16.0.0'}
'@ionic/utils-subprocess@3.0.1':
resolution: {integrity: sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==}
engines: {node: '>=16.0.0'}
'@ionic/utils-terminal@2.3.5':
resolution: {integrity: sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==}
engines: {node: '>=16.0.0'}
'@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
@@ -5857,6 +5920,9 @@ packages:
'@types/fs-extra@11.0.4':
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
'@types/fs-extra@8.1.5':
resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==}
'@types/fs-extra@9.0.13':
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
@@ -6009,6 +6075,9 @@ packages:
'@types/sinonjs__fake-timers@8.1.5':
resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==}
'@types/slice-ansi@4.0.0':
resolution: {integrity: sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==}
'@types/statuses@2.0.6':
resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==}
@@ -6941,10 +7010,6 @@ packages:
'@wxt-dev/storage@1.2.6':
resolution: {integrity: sha512-f6AknnpJvhNHW4s0WqwSGCuZAj0fjP3EVNPBO5kB30pY+3Zt/nqZGqJN6FgBLCSkYjPJ8VL1hNX5LMVmvxQoDw==}
'@xmldom/xmldom@0.8.12':
resolution: {integrity: sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==}
engines: {node: '>=10.0.0'}
'@xmldom/xmldom@0.9.9':
resolution: {integrity: sha512-qycIHAucxy/LXAYIjmLmtQ8q9GPnMbnjG1KXhWm9o5sCr6pOYDATkMPiTNa6/v8eELyqOQ2FsEqeoFYmgv/gJg==}
engines: {node: '>=14.6'}
@@ -7369,6 +7434,10 @@ packages:
bezier-js@6.1.4:
resolution: {integrity: sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==}
big-integer@1.6.52:
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
engines: {node: '>=0.6'}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
@@ -7417,6 +7486,10 @@ packages:
bplist-creator@0.0.8:
resolution: {integrity: sha512-Za9JKzD6fjLC16oX2wsXfc+qBEhJBJB1YPInoAQpMLhDuj5aVOv1baGeIQSq1Fr3OCqzvsoQcSBSwGId/Ja2PA==}
bplist-parser@0.3.2:
resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==}
engines: {node: '>= 5.10.0'}
brace-expansion@1.1.13:
resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==}
@@ -7831,6 +7904,10 @@ packages:
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
engines: {node: '>=16'}
commander@12.1.0:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@@ -8604,6 +8681,10 @@ packages:
engines: {node: '>= 12.20.55'}
hasBin: true
elementtree@0.1.7:
resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==}
engines: {node: '>= 0.4.0'}
elkjs@0.9.3:
resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==}
@@ -10446,6 +10527,10 @@ packages:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
knockout@3.5.1:
resolution: {integrity: sha512-wRJ9I4az0QcsH7A4v4l0enUpkS++MBx0BnL/68KaLzJg7x1qmbjSlwEoCNol7KTYZ+pmtI7Eh2J0Nu6/2Z5J/Q==}
@@ -11130,10 +11215,6 @@ packages:
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
engines: {node: '>=8'}
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
minipass@7.1.3:
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -11277,6 +11358,11 @@ packages:
napi-build-utils@2.0.0:
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
native-run@2.0.3:
resolution: {integrity: sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==}
engines: {node: '>=16.0.0'}
hasBin: true
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -11657,6 +11743,9 @@ packages:
resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==}
engines: {node: '>= 14'}
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
package-json@10.0.1:
resolution: {integrity: sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==}
engines: {node: '>=18'}
@@ -12521,6 +12610,11 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rimraf@6.1.3:
resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==}
engines: {node: 20 || >=22}
hasBin: true
roarr@2.15.4:
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
engines: {node: '>=8.0'}
@@ -12776,6 +12870,9 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
sax@1.1.4:
resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==}
sax@1.6.0:
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
engines: {node: '>=11.0.0'}
@@ -13472,6 +13569,9 @@ packages:
thread-stream@3.1.0:
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
through2@4.0.2:
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
@@ -13563,6 +13663,10 @@ packages:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'}
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
trigram-utils@2.0.1:
resolution: {integrity: sha512-nfWIXHEaB+HdyslAfMxSqWKDdmqY9I32jS7GnqpdWQnLH89r6A5sdk3fDVYqGAZ0CrT8ovAFSAo6HRiWcWNIGQ==}
@@ -13894,6 +13998,10 @@ packages:
resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==}
engines: {node: '>=18.12.0'}
untildify@4.0.0:
resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
engines: {node: '>=8'}
unused-filename@4.0.1:
resolution: {integrity: sha512-ZX6U1J04K1FoSUeoX1OicAhw4d0aro2qo+L8RhJkiGTNtBNkd/Fi1Wxoc9HzcVu6HfOzm0si/N15JjxFmD1z6A==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -14911,6 +15019,40 @@ snapshots:
hashery: 1.4.0
keyv: 5.6.0
'@capacitor/android@8.3.1(@capacitor/core@8.3.1)':
dependencies:
'@capacitor/core': 8.3.1
'@capacitor/cli@8.3.1':
dependencies:
'@ionic/cli-framework-output': 2.2.8
'@ionic/utils-subprocess': 3.0.1
'@ionic/utils-terminal': 2.3.5
commander: 12.1.0
debug: 4.4.3
env-paths: 2.2.1
fs-extra: 11.3.4
kleur: 4.1.5
native-run: 2.0.3
open: 8.4.2
plist: 3.1.0
prompts: 2.4.2
rimraf: 6.1.3
semver: 7.7.4
tar: 7.5.11
tslib: 2.8.1
xml2js: 0.6.2
transitivePeerDependencies:
- supports-color
'@capacitor/core@8.3.1':
dependencies:
tslib: 2.8.1
'@capacitor/splash-screen@8.0.1(@capacitor/core@8.3.1)':
dependencies:
'@capacitor/core': 8.3.1
'@catppuccin/codemirror@1.0.3':
dependencies:
'@catppuccin/palette': 1.7.1
@@ -16607,11 +16749,6 @@ snapshots:
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.9.0':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.9.1':
dependencies:
tslib: 2.8.1
@@ -17391,7 +17528,7 @@ snapshots:
'@img/sharp-wasm32@0.34.5':
dependencies:
'@emnapi/runtime': 1.9.0
'@emnapi/runtime': 1.9.2
optional: true
'@img/sharp-win32-arm64@0.34.5':
@@ -17533,6 +17670,82 @@ snapshots:
'@types/node': 24.12.2
optional: true
'@ionic/cli-framework-output@2.2.8':
dependencies:
'@ionic/utils-terminal': 2.3.5
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-array@2.1.6':
dependencies:
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-fs@3.1.7':
dependencies:
'@types/fs-extra': 8.1.5
debug: 4.4.3
fs-extra: 9.1.0
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-object@2.1.6':
dependencies:
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-process@2.1.12':
dependencies:
'@ionic/utils-object': 2.1.6
'@ionic/utils-terminal': 2.3.5
debug: 4.4.3
signal-exit: 3.0.7
tree-kill: 1.2.2
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-stream@3.1.7':
dependencies:
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-subprocess@3.0.1':
dependencies:
'@ionic/utils-array': 2.1.6
'@ionic/utils-fs': 3.1.7
'@ionic/utils-process': 2.1.12
'@ionic/utils-stream': 3.1.7
'@ionic/utils-terminal': 2.3.5
cross-spawn: 7.0.6
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-terminal@2.3.5':
dependencies:
'@types/slice-ansi': 4.0.0
debug: 4.4.3
signal-exit: 3.0.7
slice-ansi: 4.0.0
string-width: 4.2.3
strip-ansi: 6.0.1
tslib: 2.8.1
untildify: 4.0.0
wrap-ansi: 7.0.0
transitivePeerDependencies:
- supports-color
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.3
@@ -19787,6 +20000,10 @@ snapshots:
'@types/jsonfile': 6.1.4
'@types/node': 22.15.21
'@types/fs-extra@8.1.5':
dependencies:
'@types/node': 24.12.2
'@types/fs-extra@9.0.13':
dependencies:
'@types/node': 24.12.2
@@ -19948,6 +20165,8 @@ snapshots:
'@types/sinonjs__fake-timers@8.1.5': {}
'@types/slice-ansi@4.0.0': {}
'@types/statuses@2.0.6':
optional: true
@@ -21940,8 +22159,6 @@ snapshots:
async-mutex: 0.5.0
dequal: 2.0.3
'@xmldom/xmldom@0.8.12': {}
'@xmldom/xmldom@0.9.9': {}
'@xtuc/ieee754@1.2.0': {}
@@ -22364,6 +22581,8 @@ snapshots:
bezier-js@6.1.4: {}
big-integer@1.6.52: {}
binary-extensions@2.3.0: {}
bindings@1.5.0:
@@ -22432,6 +22651,10 @@ snapshots:
stream-buffers: 2.2.0
optional: true
bplist-parser@0.3.2:
dependencies:
big-integer: 1.6.52
brace-expansion@1.1.13:
dependencies:
balanced-match: 1.0.2
@@ -23024,6 +23247,8 @@ snapshots:
commander@11.1.0: {}
commander@12.1.0: {}
commander@2.20.3: {}
commander@2.9.0:
@@ -23871,6 +24096,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
elementtree@0.1.7:
dependencies:
sax: 1.1.4
elkjs@0.9.3: {}
emitter-listener@1.1.2:
@@ -25018,7 +25247,7 @@ snapshots:
glob@13.0.0:
dependencies:
minimatch: 10.2.4
minipass: 7.1.2
minipass: 7.1.3
path-scurry: 2.0.0
glob@13.0.6:
@@ -26127,6 +26356,8 @@ snapshots:
kleur@3.0.3: {}
kleur@4.1.5: {}
knockout@3.5.1: {}
kolorist@1.8.0: {}
@@ -27057,8 +27288,6 @@ snapshots:
dependencies:
yallist: 4.0.0
minipass@7.1.2: {}
minipass@7.1.3: {}
minizlib@2.1.2:
@@ -27199,6 +27428,22 @@ snapshots:
napi-build-utils@2.0.0: {}
native-run@2.0.3:
dependencies:
'@ionic/utils-fs': 3.1.7
'@ionic/utils-terminal': 2.3.5
bplist-parser: 0.3.2
debug: 4.4.3
elementtree: 0.1.7
ini: 4.1.3
plist: 3.1.0
split2: 4.2.0
through2: 4.0.2
tslib: 2.8.1
yauzl: 2.10.0
transitivePeerDependencies:
- supports-color
natural-compare@1.4.0: {}
needle@3.5.0:
@@ -27638,6 +27883,8 @@ snapshots:
degenerator: 5.0.1
netmask: 2.0.2
package-json-from-dist@1.0.1: {}
package-json@10.0.1:
dependencies:
ky: 1.14.2
@@ -27867,7 +28114,7 @@ snapshots:
plist@3.1.0:
dependencies:
'@xmldom/xmldom': 0.8.12
'@xmldom/xmldom': 0.9.9
base64-js: 1.5.1
xmlbuilder: 15.1.1
@@ -28589,6 +28836,11 @@ snapshots:
dependencies:
glob: 7.2.3
rimraf@6.1.3:
dependencies:
glob: 13.0.6
package-json-from-dist: 1.0.1
roarr@2.15.4:
dependencies:
boolean: 3.2.0
@@ -28885,6 +29137,8 @@ snapshots:
'@parcel/watcher': 2.5.6
optional: true
sax@1.1.4: {}
sax@1.6.0: {}
saxes@6.0.0:
@@ -29763,6 +30017,10 @@ snapshots:
dependencies:
real-require: 0.2.0
through2@4.0.2:
dependencies:
readable-stream: 3.6.2
through@2.3.8: {}
time2fa@1.4.2: {}
@@ -29848,6 +30106,8 @@ snapshots:
punycode: 2.3.1
optional: true
tree-kill@1.2.2: {}
trigram-utils@2.0.1:
dependencies:
collapse-white-space: 2.1.0
@@ -30187,6 +30447,8 @@ snapshots:
picomatch: 4.0.4
webpack-virtual-modules: 0.6.2
untildify@4.0.0: {}
unused-filename@4.0.1:
dependencies:
escape-string-regexp: 5.0.0

View File

@@ -76,4 +76,61 @@ magick "./png/256x256-dev.png" -background "#ffffff" -gravity center -extent 640
# Copy server assets
server_dir="$script_dir/../../apps/server"
cp "$desktop_forge_dir/app-icon/icon.ico" "$server_dir/src/assets/icon.ico"
cp "$desktop_forge_dir/app-icon/icon-dev.ico" "$server_dir/src/assets/icon-dev.ico"
cp "$desktop_forge_dir/app-icon/icon-dev.ico" "$server_dir/src/assets/icon-dev.ico"
# Build Android mobile icons
# Legacy launcher: 48/72/96/144/192 px. Adaptive foreground: 108dp canvas with
# ~66% safe zone (Android masks the outer ring), scaled per density.
mobile_res_dir="$script_dir/../../apps/mobile/android/app/src/main/res"
background_color="#FAFAFA"
# Circular mask rendered via Inkscape for crisp antialiasing at icon sizes.
circle_mask_svg=$(mktemp --suffix=.svg)
cat > "$circle_mask_svg" <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="50" fill="white"/>
</svg>
EOF
trap 'rm -f "$circle_mask_svg"' EXIT
declare -A launcher_sizes=( [mdpi]=48 [hdpi]=72 [xhdpi]=96 [xxhdpi]=144 [xxxhdpi]=192 )
declare -A foreground_sizes=( [mdpi]=108 [hdpi]=162 [xhdpi]=216 [xxhdpi]=324 [xxxhdpi]=432 )
for density in mdpi hdpi xhdpi xxhdpi xxxhdpi; do
launcher_size=${launcher_sizes[$density]}
foreground_size=${foreground_sizes[$density]}
mipmap_dir="$mobile_res_dir/mipmap-$density"
mkdir -p "$mipmap_dir"
# Adaptive foreground: logo at 50% of the 108dp canvas. The 72dp (66%) safe
# zone is the hard clip boundary — any launcher mask (circle, squircle,
# teardrop) can trim up to it, so we leave extra margin inside.
fg_logo=$(( foreground_size / 2 ))
inkscape -w $fg_logo -h $fg_logo "$source_icon_dir/icon-color.svg" -o "$mipmap_dir/_tmp_fg.png"
magick "$mipmap_dir/_tmp_fg.png" -background none -gravity center \
-extent ${foreground_size}x${foreground_size} "$mipmap_dir/ic_launcher_foreground.png"
rm "$mipmap_dir/_tmp_fg.png"
# Monochrome layer for Android 13+ themed icons. Android ignores RGB and
# uses only the alpha channel, tinting it with the system theme color.
inkscape -w $fg_logo -h $fg_logo "$source_icon_dir/icon-black.svg" -o "$mipmap_dir/_tmp_mono.png"
magick "$mipmap_dir/_tmp_mono.png" -background none -gravity center \
-extent ${foreground_size}x${foreground_size} "$mipmap_dir/ic_launcher_monochrome.png"
rm "$mipmap_dir/_tmp_mono.png"
# Legacy square launcher (logo on solid background)
sq_logo=$(( launcher_size * 2 / 3 ))
inkscape -w $sq_logo -h $sq_logo "$source_icon_dir/icon-color.svg" -o "$mipmap_dir/_tmp.png"
magick "$mipmap_dir/_tmp.png" -background "$background_color" -gravity center \
-extent ${launcher_size}x${launcher_size} "$mipmap_dir/ic_launcher.png"
# Legacy round launcher: Inkscape-rendered circle used as alpha mask.
# Extract the mask's alpha channel (circle=opaque, bg=transparent) and copy
# it onto the square icon.
inkscape -w $launcher_size -h $launcher_size "$circle_mask_svg" \
-o "$mipmap_dir/_tmp_mask.png"
magick "$mipmap_dir/ic_launcher.png" \
\( "$mipmap_dir/_tmp_mask.png" -alpha extract \) \
-compose CopyOpacity -composite "$mipmap_dir/ic_launcher_round.png"
rm "$mipmap_dir/_tmp.png" "$mipmap_dir/_tmp_mask.png"
done