Vibecoded CI/CD
3
frontend/.gitignore
vendored
@@ -6,6 +6,9 @@
|
||||
.cache
|
||||
dist
|
||||
|
||||
# The desktop bundle (nuxt generate in its desktop shape, read by src-tauri/)
|
||||
dist-desktop
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
A session is a bearer token the API handed us, kept in a cookie rather than
|
||||
localStorage so the server can see it too: the sidebar then knows who is
|
||||
signed in on the very first draw, before any script runs. The token itself is
|
||||
never decoded here — the API is asked who it belongs to (see restoreSession). */
|
||||
never decoded here — the API is asked who it belongs to (see restoreSession).
|
||||
|
||||
In the desktop build the token is kept twice — see keepTokenRemembered, at
|
||||
the foot of this file. */
|
||||
|
||||
export interface AuthUser {
|
||||
id: string
|
||||
@@ -34,6 +37,11 @@ export function useAuth() {
|
||||
maxAge: WEEK_IN_SECONDS,
|
||||
sameSite: 'lax',
|
||||
})
|
||||
/* A window is not owed a cookie jar: its own address is one a webview is not
|
||||
everywhere obliged to store cookies against, and a session that could not
|
||||
outlive the window would ask the person to sign in every time they opened
|
||||
the app. A no-op anywhere else. */
|
||||
keepTokenRemembered(token, TOKEN_COOKIE)
|
||||
const user = useState<AuthUser | null>('auth:user', () => null)
|
||||
|
||||
const loggedIn = computed(() => user.value !== null)
|
||||
|
||||
65
frontend/app/plugins/desktop.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/* The window's own habits.
|
||||
|
||||
These are the two things the wiki does differently once it is a desktop app
|
||||
rather than a page of the web — see utils/desktop.ts for the asking, and
|
||||
src-tauri/ for the shell. The web build takes the first line and returns:
|
||||
nothing here changes how a browser sees the wiki.
|
||||
|
||||
Runs after the auth plugin (plugins come in name order), which matters: by
|
||||
now a signed-in person's account has already had its say about the interface
|
||||
language, and an account outranks this device. */
|
||||
|
||||
import { isDesktop, leadsOutward, openExternal, storeGet, storeSet } from '~/utils/desktop'
|
||||
|
||||
/** Where a window keeps the interface language it was last shown. Also the
|
||||
* name of the cookie the web build writes it to — the same choice, remembered
|
||||
* the way this kind of host can be sure of remembering it. */
|
||||
const LOCALE_KEY = 'mathew-locale'
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
if (!import.meta.client || !isDesktop()) return
|
||||
|
||||
/* Links. The wiki's own addresses are relative and the router turns them as
|
||||
it always does; an absolute one is about something outside this window —
|
||||
and a window has one page, no tab strip, no back button a reader would
|
||||
trust after the wiki had been replaced by some other site's document. So
|
||||
those go out to the system's browser instead, and the window keeps
|
||||
showing the wiki it was asked to show.
|
||||
|
||||
Captured on the way in, so it also catches the links drawn from article
|
||||
Markdown — the rendered text belongs to the article, which has no reason
|
||||
to know it is not in a browser. */
|
||||
document.addEventListener(
|
||||
'click',
|
||||
(event) => {
|
||||
if (event.defaultPrevented || event.button !== 0) return
|
||||
// a modified click is the reader asking for the browser's own gesture
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
|
||||
const target = event.target as Element | null
|
||||
const href = target?.closest?.('a')?.getAttribute('href')
|
||||
if (!href || !leadsOutward(href)) return
|
||||
event.preventDefault()
|
||||
void openExternal(href)
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
/* The interface language, kept where a window can be certain of keeping it.
|
||||
On the web the choice rides on the `mathew-locale` cookie, which the server
|
||||
reads so the first draw already speaks it. A window draws nothing on a
|
||||
server, and its own address — `tauri://localhost` — is one a webview is
|
||||
not everywhere obliged to keep cookies for, so the choice is mirrored into
|
||||
localStorage: a lost cookie costs a language switch at the next start, and
|
||||
that is all.
|
||||
|
||||
An account's own choice outranks the mirror, as it outranks the cookie —
|
||||
hence the guard, the auth plugin having run first. Whatever the UI ends up
|
||||
speaking is then written back, so the mirror always says what this window
|
||||
last showed. */
|
||||
const { locale, choose } = useAppLocale()
|
||||
const { user } = useAuth()
|
||||
const remembered = storeGet(LOCALE_KEY)
|
||||
if (remembered && !user.value?.locale) void choose(remembered)
|
||||
|
||||
watch(locale, (code) => storeSet(LOCALE_KEY, code))
|
||||
})
|
||||
116
frontend/app/utils/desktop.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/* Running as a desktop app.
|
||||
|
||||
One bundle answers both a browser and an app window (Tauri — see
|
||||
src-tauri/), so nothing in `app/` is desktop code: it is the wiki asking at
|
||||
run time whether it happens to be sitting in a window. The answer comes from
|
||||
the shell, which marks the page it loads and marks no other — so the same
|
||||
dist-desktop/ files opened in an ordinary browser behave as a web page, and
|
||||
a web page that somehow met Tauri would only gain the shell's habits.
|
||||
|
||||
What a window does differently, then, is small and all about being a window
|
||||
with one page and no browser chrome: a link leads out to the system's own
|
||||
browser rather than replacing the wiki in the only view there is, and the
|
||||
few bytes the app remembers of a person live where a window can be certain
|
||||
of keeping them. */
|
||||
|
||||
import type { Ref } from 'vue'
|
||||
import { watch } from 'vue'
|
||||
|
||||
/** The handle every Tauri webview injects into its page — present before the
|
||||
* npm API is imported, and without it. Nothing else sets this name. */
|
||||
export function isDesktop(): boolean {
|
||||
if (!import.meta.client) return false
|
||||
return (window as Window & { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ != null
|
||||
}
|
||||
|
||||
/* A window is expected to have localStorage. A private browser may throw on
|
||||
being asked for it at all, and a setting that cannot be remembered is worth
|
||||
far less than a page that still draws — so the three doors below are the
|
||||
only place any of it is touched, and none of them lets a throw through. */
|
||||
|
||||
export function storeGet(key: string): string | null {
|
||||
try {
|
||||
return localStorage.getItem(key)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function storeSet(key: string, value: string): void {
|
||||
try {
|
||||
localStorage.setItem(key, value)
|
||||
} catch {
|
||||
/* nothing remembered; nothing else depends on it */
|
||||
}
|
||||
}
|
||||
|
||||
export function storeClear(key: string): void {
|
||||
try {
|
||||
localStorage.removeItem(key)
|
||||
} catch {
|
||||
/* as good as cleared, for as long as this page lives */
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether an address leads outside this app — to a document on some other
|
||||
* site, or to a mail client, or to a phone dialler. The app's own pages are
|
||||
* the relative `/wiki/…` the router already knows; an absolute address that
|
||||
* happens to name the wiki's public site counts as outward too, and opens
|
||||
* there in the browser, which is the honest reading of a link written out in
|
||||
* full. */
|
||||
export function leadsOutward(href: string): boolean {
|
||||
if (!import.meta.client || !href) return false
|
||||
let url: URL
|
||||
try {
|
||||
url = new URL(href, window.location.href)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
if (url.protocol === 'mailto:' || url.protocol === 'tel:') return true
|
||||
return (url.protocol === 'http:' || url.protocol === 'https:') && url.origin !== window.location.origin
|
||||
}
|
||||
|
||||
/** Hand a document to whatever machine-level application owns it: the browser
|
||||
* for a page, the mail client for `mailto:`. In a browser a link already does
|
||||
* this, so the fallback is the browser's own new tab; in a window there is no
|
||||
* other tab, and following the link in place would leave the wiki's page
|
||||
* gone from the only view there is. */
|
||||
export async function openExternal(url: string): Promise<void> {
|
||||
if (!import.meta.client || !url) return
|
||||
if (isDesktop()) {
|
||||
try {
|
||||
const { openUrl } = await import('@tauri-apps/plugin-opener')
|
||||
await openUrl(url)
|
||||
return
|
||||
} catch {
|
||||
/* no shell to hand it to, or this window is not allowed this one — the
|
||||
least worse thing left is the browser's way out */
|
||||
}
|
||||
}
|
||||
window.open(url, '_blank', 'noopener')
|
||||
}
|
||||
|
||||
/** Keep a session token where a window cannot lose it.
|
||||
|
||||
On the web a cookie is the whole of it: the server reads it, and the browser
|
||||
keeps it as long as the API said the token lasts. A window's own address
|
||||
(`tauri://localhost`) is one a webview is not everywhere obliged to keep
|
||||
cookies for, and a session dying with the window would mean signing in again
|
||||
every time the app was opened — for a token the API hands out for a week. So
|
||||
here it is mirrored into localStorage and taken back from there whenever the
|
||||
cookie comes up empty, whichever of the two this webview chose to keep.
|
||||
|
||||
The cookie still leads: it is the one already at hand, the one a web build
|
||||
would have read, and the one the sign-out line clears. A browser asked this
|
||||
question of the same files ignores all of it — there is nothing to mirror
|
||||
where cookies work — so the wiki's behavior there is untouched. */
|
||||
export function keepTokenRemembered(token: Ref<string | null>, key: string): void {
|
||||
if (!import.meta.client || !isDesktop()) return
|
||||
|
||||
if (!token.value) {
|
||||
const kept = storeGet(key)
|
||||
if (kept) token.value = kept
|
||||
}
|
||||
// sync, so signing out clears the mirror in the same breath, not on a tick
|
||||
watch(token, (value) => (value ? storeSet(key, value) : storeClear(key)), { flush: 'sync' })
|
||||
}
|
||||
36
frontend/desktop/build.mjs
Normal file
@@ -0,0 +1,36 @@
|
||||
/* The bundle the desktop app loads.
|
||||
|
||||
nuxt.config.ts reads a MATHEW_DESKTOP flag and, in that shape, writes the
|
||||
wiki as plain files — no server, no `/api` proxy, since a window has neither
|
||||
— into dist-desktop/public, which is the `frontendDist` src-tauri/tauri.conf.json
|
||||
names. `tauri build` gets here through `npm run desktop:generate`, which is
|
||||
that config's `beforeBuildCommand`.
|
||||
|
||||
The reason this file exists rather than a script line: `MATHEW_DESKTOP=1 nuxt
|
||||
generate` is a bashism, and desktop builds are something Windows is asked for
|
||||
too. Setting the variable here works from any shell.
|
||||
|
||||
The wiki the app ships pointed at is overridable like any other build
|
||||
setting — set it in the environment before `npm run desktop:build`:
|
||||
NUXT_PUBLIC_API_BASE=http://localhost:5000 a wiki on this machine
|
||||
NUXT_PUBLIC_LEAN_URL=https://lean.example a Lean server the app can reach
|
||||
(Lean's default is empty, which leaves /lean saying there is no server here
|
||||
rather than reaching for a localhost that is not on the machine). */
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
||||
|
||||
const run = spawnSync(npm, ['exec', '--', 'nuxt', 'generate'], {
|
||||
cwd: fileURLToPath(new URL('..', import.meta.url)),
|
||||
env: { ...process.env, MATHEW_DESKTOP: '1' },
|
||||
stdio: 'inherit',
|
||||
})
|
||||
|
||||
if (run.error) {
|
||||
console.error(`desktop: could not run \`nuxt generate\` — ${run.error.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
process.exit(run.status ?? 1)
|
||||
BIN
frontend/desktop/icon-source.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
88
frontend/desktop/make-icon.py
Normal file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Draw desktop/icon-source.png — the 1024² master the icon set is cut from.
|
||||
|
||||
The wiki's own mark is the ∑ in the sidebar (AppHeader.vue's .logo-mark) over
|
||||
the dark reading surface, so the app icon is that: the sigma, in the accent the
|
||||
wiki wears by default, on the dark navy of its dark theme.
|
||||
|
||||
python3 desktop/make-icon.py # writes desktop/icon-source.png
|
||||
npm run desktop:icons # cuts src-tauri/icons/ from it
|
||||
|
||||
Requires Pillow (pip install pillow). Nothing else in the project uses it —
|
||||
this is build tooling, not app code.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
SIZE = 1024
|
||||
PAD = 64 # the margin macOS wants round an app icon; harmless elsewhere
|
||||
RADIUS = 200
|
||||
TOP = (16, 21, 44) # the navy a touch above --bg, so the square reads as an object
|
||||
BOTTOM = (6, 8, 14) # ... down to just under it
|
||||
ACCENT = (124, 140, 255) # --accent, dark theme (#7c8cff)
|
||||
MARK = "\u2211" # ∑ — the wiki's mark
|
||||
|
||||
FONTS = [
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||
"/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf",
|
||||
]
|
||||
|
||||
OUT = Path(__file__).resolve().parent / "icon-source.png"
|
||||
|
||||
|
||||
def load_font(size: int) -> ImageFont.FreeTypeFont:
|
||||
"""The first system font that actually carries a sigma."""
|
||||
for path in FONTS:
|
||||
try:
|
||||
font = ImageFont.truetype(path, size)
|
||||
except OSError:
|
||||
continue
|
||||
if font.getbbox(MARK)[2] > 0:
|
||||
return font
|
||||
raise SystemExit(f"no font with a {MARK} found — look for one of: {', '.join(FONTS)}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# vertical gradient, then clipped to a rounded square
|
||||
gradient = Image.new("RGB", (1, SIZE))
|
||||
for y in range(SIZE):
|
||||
t = y / (SIZE - 1)
|
||||
gradient.putpixel((0, y), tuple(round(a + (b - a) * t) for a, b in zip(TOP, BOTTOM)))
|
||||
gradient = gradient.resize((SIZE, SIZE))
|
||||
|
||||
mask = Image.new("L", (SIZE, SIZE), 0)
|
||||
ImageDraw.Draw(mask).rounded_rectangle(
|
||||
[PAD, PAD, SIZE - PAD, SIZE - PAD], radius=RADIUS, fill=255
|
||||
)
|
||||
|
||||
icon = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
|
||||
icon.paste(gradient, (0, 0), mask)
|
||||
|
||||
draw = ImageDraw.Draw(icon)
|
||||
# a hairline of accent light along the edge, so the dark square does not
|
||||
# vanish into a dark taskbar
|
||||
draw.rounded_rectangle(
|
||||
[PAD, PAD, SIZE - PAD, SIZE - PAD], radius=RADIUS, outline=(124, 140, 255, 56), width=3
|
||||
)
|
||||
|
||||
font = load_font(600)
|
||||
left, top, right, bottom = draw.textbbox((0, 0), MARK, font=font)
|
||||
w, h = right - left, bottom - top
|
||||
# centred on the box the glyph is drawn in, not on its typographic cell:
|
||||
# an ascent-based centre would sit the sigma high
|
||||
draw.text(
|
||||
((SIZE - w) / 2 - left, (SIZE - h) / 2 - top),
|
||||
MARK,
|
||||
font=font,
|
||||
fill=ACCENT + (255,),
|
||||
)
|
||||
|
||||
icon.save(OUT)
|
||||
print(f"wrote {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,4 +1,24 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
|
||||
/* The desktop build — the same wiki in an app window (Tauri, see src-tauri/).
|
||||
A window has no Nuxt server and no nginx behind it, so the wiki is written
|
||||
out as plain files for the system webview to read off disk and every address
|
||||
it calls has to be absolute. `desktop/build.mjs` sets the flag; the Tauri CLI
|
||||
carries TAURI_ENV_* into its own build hooks, so `tauri build` reaches this
|
||||
branch without anyone remembering to export anything. The web build takes
|
||||
none of it: same config file, different shape of output. */
|
||||
const desktop = process.env.MATHEW_DESKTOP === '1' || !!process.env.TAURI_ENV_PLATFORM
|
||||
|
||||
/* Where a desktop build takes its wiki from. Baked in at build — the app has
|
||||
no origin of its own to route `/api` through — and overridable for anyone
|
||||
building against another wiki: NUXT_PUBLIC_API_BASE (a staging wiki, this
|
||||
machine), and NUXT_PUBLIC_LEAN_URL for a Lean server the app can actually
|
||||
reach. Lean's default stays empty on purpose: a window on a random laptop
|
||||
has no localhost:8080 to find, and /lean says so plainly rather than
|
||||
reaching for a server that is not there. */
|
||||
const apiDefault = desktop ? 'https://mathew.aranroig.com' : 'http://localhost:5000'
|
||||
const leanDefault = desktop ? '' : 'http://localhost:8080'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: true },
|
||||
@@ -37,26 +57,43 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
|
||||
/* A page of the web is drawn on the server; a window on the machine cannot
|
||||
be — the webview draws these same pages itself, from the bundle. The book,
|
||||
the middleware and the popups all run client-side either way, so nothing
|
||||
but where HTML first comes from changes. */
|
||||
ssr: !desktop,
|
||||
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
// Where the UI calls the API. The Docker image sets this to "" so the
|
||||
// browser calls /api on its own origin and nginx routes it to the
|
||||
// backend; dev keeps the absolute localhost address.
|
||||
apiBase: process.env.NUXT_PUBLIC_API_BASE ?? 'http://localhost:5000',
|
||||
// backend; dev keeps the absolute localhost address, and the desktop
|
||||
// build the public wiki it ships pointed at.
|
||||
apiBase: process.env.NUXT_PUBLIC_API_BASE ?? apiDefault,
|
||||
// Where the Lean editor server (lean4web — see lean/) lives: /lean
|
||||
// frames this address. It is the browser's own way in, not the
|
||||
// server's — the iframe talks to its own origin from there on.
|
||||
leanUrl: process.env.NUXT_PUBLIC_LEAN_URL ?? 'http://localhost:8080',
|
||||
leanUrl: process.env.NUXT_PUBLIC_LEAN_URL ?? leanDefault,
|
||||
},
|
||||
},
|
||||
|
||||
/* The desktop bundle: files under dist-desktop/ and nothing to run. Written
|
||||
there rather than into .output/ so a desktop build never overwrites the
|
||||
server bundle a web build left, and `frontendDist` in tauri.conf.json has
|
||||
a directory of its own to read. */
|
||||
...(desktop
|
||||
? { nitro: { preset: 'static', output: { dir: 'dist-desktop' } } }
|
||||
: {}),
|
||||
|
||||
// The Nuxt server's own API calls (SSR) are relative once apiBase is "",
|
||||
// and land on this Nitro route, which forwards them to the backend. The
|
||||
// Docker image bakes NUXT_API_PROXY_TARGET=https://mathew.aranroig.com in
|
||||
// at build; nginx routes that address on to the backend.
|
||||
routeRules: {
|
||||
'/api/**': { proxy: `${process.env.NUXT_API_PROXY_TARGET || 'http://localhost:5000'}/api/**` },
|
||||
},
|
||||
// at build; nginx routes that address on to the backend. A desktop build has
|
||||
// no Nuxt server to answer this, and no reason for one: the app calls the
|
||||
// API by its own absolute address.
|
||||
routeRules: desktop
|
||||
? {}
|
||||
: { '/api/**': { proxy: `${process.env.NUXT_API_PROXY_TARGET || 'http://localhost:5000'}/api/**` } },
|
||||
|
||||
app: {
|
||||
head: {
|
||||
@@ -68,14 +105,25 @@ export default defineNuxtConfig({
|
||||
{ name: 'theme-color', media: '(prefers-color-scheme: light)', content: '#f7f8fb' },
|
||||
{ name: 'theme-color', media: '(prefers-color-scheme: dark)', content: '#0a0c12' },
|
||||
],
|
||||
script: [
|
||||
{
|
||||
// Apply saved theme & accent before first paint to avoid a flash
|
||||
tagPosition: 'head',
|
||||
innerHTML:
|
||||
"(function(){try{var d=document.documentElement;var s=localStorage.getItem('mathew-theme');var t=s||(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');d.setAttribute('data-theme',t);var a=localStorage.getItem('mathew-accent');if(a&&a!=='violet')d.setAttribute('data-accent',a);}catch(e){}})();",
|
||||
},
|
||||
],
|
||||
script: desktop
|
||||
? [
|
||||
/* The same pre-paint, from a file: a window is a host one may
|
||||
sensibly hold to a `script-src 'self'` Content Security Policy,
|
||||
and code written into the HTML would not survive one.
|
||||
public/preboot.js carries this exact code — keep the pair in
|
||||
step. (src-tauri/ sets no policy yet; keeping this one in a
|
||||
file is what makes turning it on a config line, not a page
|
||||
rewrite.) */
|
||||
{ tagPosition: 'head', src: '/preboot.js' },
|
||||
]
|
||||
: [
|
||||
{
|
||||
// Apply saved theme & accent before first paint to avoid a flash
|
||||
tagPosition: 'head',
|
||||
innerHTML:
|
||||
"(function(){try{var d=document.documentElement;var s=localStorage.getItem('mathew-theme');var t=s||(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');d.setAttribute('data-theme',t);var a=localStorage.getItem('mathew-accent');if(a&&a!=='violet')d.setAttribute('data-accent',a);}catch(e){}})();",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
253
frontend/package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxtjs/i18n": "^10.6.0",
|
||||
"@tauri-apps/plugin-opener": "^2.7.0",
|
||||
"highlight.js": "^11.12.0",
|
||||
"isomorphic-dompurify": "^4.4.0",
|
||||
"katex": "^0.18.9",
|
||||
@@ -19,6 +20,7 @@
|
||||
"vue-router": "^5.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.12.1",
|
||||
"@types/katex": "^0.16.8",
|
||||
"@types/markdown-it": "^14.2.0",
|
||||
"sass": "^1.105.0"
|
||||
@@ -4756,6 +4758,257 @@
|
||||
"integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.12.1.tgz",
|
||||
"integrity": "sha512-DeyFHa3wynpyoqTDikDEDGTJIq4LQ5USfolQGRmGIWT6JMADyxZBTDa5cAdT3tDg73rUXufPaCwN7aXBos4OnQ==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.12.1.tgz",
|
||||
"integrity": "sha512-kEDEiGzG+yAc5FeLxtXpES/VN+F2C8H0r4gDVtfRLKxT9np9101d9REG/Kgo2lr0hKi0IpDsODiHnU3+naOmLg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
"tauri": "tauri.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.12.1",
|
||||
"@tauri-apps/cli-darwin-x64": "2.12.1",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.12.1",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.12.1",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.12.1",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.12.1",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.12.1",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.12.1",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.12.1",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.12.1",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.12.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.12.1.tgz",
|
||||
"integrity": "sha512-8bMnbpJ2jKO/ufrOPL65EMHC3ptw7fzeraoQkwibyfGLqIl+UCWsC0152kmNbW6r9NvVQduXqAZF5mH1QgTWhw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.12.1.tgz",
|
||||
"integrity": "sha512-G+2qte8GNqNlEZ9ftd21rbu/APVkl3r/nzNDI7LVxEoqJWVNe9GbEgQUMH1r4PHT20Z2UxR3KjCkltIb0e/Wjg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.12.1.tgz",
|
||||
"integrity": "sha512-5b3n4DaTW+A1Zntnh4Yn44VyLrlPEpB8kMOBRfWkzNnzolBk+AYhKIfKjCLmoC8Mob3ETodxAEOMkE2YPKEWzQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.12.1.tgz",
|
||||
"integrity": "sha512-APArHhDu+8pXSCkCQmvaz1nD3d8WTmlCoSS/oG475TJ6AATBvyQOAmf5qELgsQ4s2+8MOsop0Zd699RKELF1Cw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.12.1.tgz",
|
||||
"integrity": "sha512-JaCxcLoV0oGtllVkLr4UAHWG/wTfkEunnUqnsEsGYOp0VRxnUwHg63wFBD0jU2ul5eFfSLIDS4RF2gfzsSo5Jg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.12.1.tgz",
|
||||
"integrity": "sha512-K5+VXM7+SGVDwZleA2+lMgvbTVuA5e1tkiVEawu1Yxj26ZyEfO8h8xTC1rBdRX2xgJDSwCk6vPUBCS6i+Z5slQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.12.1.tgz",
|
||||
"integrity": "sha512-Z1QGPr49HJZMktu+Spu6Q1b/f9ANadXvqZZZt2645ua1x1ev6prOgp85eiGv2jRzohFEMLo6aJyaLjLUX5KIvA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.12.1.tgz",
|
||||
"integrity": "sha512-nHuEhRPOMSJ/8RRhtMRxB9B9lVr7xxpe4DapBz8x1GIau3vdzjHHJ1hAfmw+PyayPLZ6XoaVIHPgI17rX6ii8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.12.1.tgz",
|
||||
"integrity": "sha512-I0oPk2uGh86R8dAs2X5OxSP+/GqSc8jh1Px3l8EqrocSNivsRHDTh2k3PVOqdv163dlV6PIVvmhZAbH/lPfZrA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.12.1.tgz",
|
||||
"integrity": "sha512-baNZRBfepAJ5qNFRMwzgBk94xQmkcmTZrNDjZjnzrq2vAaTC3NAhallb/zo3hyoCGQlMS13sdovktrg46NZz6Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.12.1.tgz",
|
||||
"integrity": "sha512-dIFaKslrzcbesb+bBh+E1R9km241pHyj32w6D9FPtvQeYpRUFbiCLEa/2S+S0UnIytfxepp4P+X8IerGqnXeYg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-opener": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.7.0.tgz",
|
||||
"integrity": "sha512-mBorYfVKh9Lt7s4ZFtKbMUTxBw+Y/sVmSlKCtkkoBgMTC/DIbt/kaPm67L/2djAUqDI1wbixy5OHHVmipjuGMw==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz",
|
||||
|
||||
@@ -7,10 +7,15 @@
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
"postinstall": "nuxt prepare",
|
||||
"desktop:generate": "node desktop/build.mjs",
|
||||
"desktop:dev": "tauri dev",
|
||||
"desktop:build": "tauri build",
|
||||
"desktop:icons": "tauri icon desktop/icon-source.png"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxtjs/i18n": "^10.6.0",
|
||||
"@tauri-apps/plugin-opener": "^2.7.0",
|
||||
"highlight.js": "^11.12.0",
|
||||
"isomorphic-dompurify": "^4.4.0",
|
||||
"katex": "^0.18.9",
|
||||
@@ -22,6 +27,7 @@
|
||||
"vue-router": "^5.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.12.1",
|
||||
"@types/katex": "^0.16.8",
|
||||
"@types/markdown-it": "^14.2.0",
|
||||
"sass": "^1.105.0"
|
||||
|
||||
17
frontend/public/preboot.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/* Apply saved theme & accent before first paint to avoid a flash.
|
||||
*
|
||||
* This is the desktop build's copy of the inline snippet nuxt.config.ts puts
|
||||
* in <head> for the web (keep the two in step). Out here as a file because a
|
||||
* window may be held to a Content Security Policy — see src-tauri/README.md —
|
||||
* and `script-src 'self'` would have no room for a script written inline in
|
||||
* the HTML.
|
||||
* A classic <script src> in <head> still runs before the page paints, so the
|
||||
* surfaces arrive already the right way round. */
|
||||
try {
|
||||
var d = document.documentElement
|
||||
var s = localStorage.getItem('mathew-theme')
|
||||
var t = s || (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
||||
d.setAttribute('data-theme', t)
|
||||
var a = localStorage.getItem('mathew-accent')
|
||||
if (a && a !== 'violet') d.setAttribute('data-accent', a)
|
||||
} catch (e) {}
|
||||
6
frontend/src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Rust's build output — hundreds of MB, never belongs in the wiki.
|
||||
target/
|
||||
|
||||
# Written by tauri-build on every build: the JSON schemas for the permissions
|
||||
# listed under capabilities/. Generated, so regenerable.
|
||||
gen/schemas
|
||||
4912
frontend/src-tauri/Cargo.lock
generated
Normal file
27
frontend/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "mathew"
|
||||
version = "0.1.0"
|
||||
description = "Mathew — a wiki for mathematics, in a window of its own"
|
||||
authors = ["Aran Roig"]
|
||||
edition = "2021"
|
||||
|
||||
# The app is a library with a thin main: Tauri's own entry points want it that
|
||||
# way, so the same `run()` also serves a mobile build should one ever be asked
|
||||
# for (the window itself is desktop-shaped — see src/lib.rs).
|
||||
[lib]
|
||||
name = "mathew_desktop_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
# A link inside the window hands its document to the system's browser rather
|
||||
# than replacing the wiki in the only view there is. The wiki asks for this
|
||||
# itself (app/utils/desktop.ts); this is the door it goes out by.
|
||||
tauri-plugin-opener = "2"
|
||||
|
||||
# Only a desktop app can be launched twice onto one window.
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-single-instance = "2"
|
||||
78
frontend/src-tauri/README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# src-tauri/ — the app window
|
||||
|
||||
Mathew as a **desktop app** (Tauri 2, Linux / Windows / macOS): the wiki in a
|
||||
window of its own, no browser around it. The shell is deliberately thin — the
|
||||
app *is* the same Nuxt bundle the website serves, written out as plain files
|
||||
and embedded in the binary. Nothing in `frontend/app/` is desktop code; the
|
||||
bundle asks at run time whether it happens to be sitting in a window
|
||||
(`app/utils/desktop.ts`) and only then takes the window's habits: a link leads
|
||||
out to the system's browser instead of replacing the wiki in the only view
|
||||
there is, and the session token and interface language are mirrored into
|
||||
`localStorage` because a webview is not everywhere obliged to keep cookies for
|
||||
its own `tauri://localhost` address.
|
||||
|
||||
The wiki needs a wiki to talk to: the window holds no server and no database.
|
||||
It calls the **public Mathew** over HTTPS by default — the API base is baked in
|
||||
at build, see the table below.
|
||||
|
||||
## Build & run
|
||||
|
||||
From `frontend/` — these need [Rust](https://rustup.rs) (stable) and the
|
||||
[Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for your OS;
|
||||
on Linux that is the WebKitGTK stack:
|
||||
`libwebkit2gtk-4.1-dev libgtk-3-dev libssl-dev libxdo-dev librsvg2-dev build-essential`.
|
||||
|
||||
| Script | What it does |
|
||||
| --- | --- |
|
||||
| `npm run desktop:dev` | `tauri dev` — opens the **Nuxt dev server** (`localhost:3000`, started for you) in the window, hot-reload included; `isDesktop()` is already true inside it |
|
||||
| `npm run desktop:build` | `tauri build` — runs `desktop:generate` (below), compiles the shell, embeds the files, and leaves installers under `src-tauri/target/release/bundle/` (deb/rpm/AppImage on Linux, NSIS/MSI on Windows, .app/.dmg on macOS) |
|
||||
| `npm run desktop:generate` | the bundle the window loads: `MATHEW_DESKTOP=1 nuxt generate` → `dist-desktop/public`, the `frontendDist` this config names (a desktop build never overwrites the web `.output/`) |
|
||||
| `npm run desktop:icons` | re-cuts `src-tauri/icons/` from `desktop/icon-source.png` (redraw that one with `python3 desktop/make-icon.py`) |
|
||||
|
||||
The desktop shape lives in `nuxt.config.ts` under one flag
|
||||
(`MATHEW_DESKTOP=1`, also implied by the `TAURI_ENV_*` the Tauri CLI passes to
|
||||
its build hooks): `ssr: false`, no `/api` proxy route, and the pre-paint theme
|
||||
script served from `public/preboot.js` instead of inline. The web build takes
|
||||
none of it. Deep routes (`/wiki/en/bayes-theorem`, …) are not files in the
|
||||
bundle — Tauri's own asset serving answers unknown paths with `index.html`, so
|
||||
the client router takes over exactly as it does on a static host.
|
||||
|
||||
**Dev-server port**: `devUrl` is fixed to `3000`, and Nuxt falls back to `3001`
|
||||
when `3000` is taken (see the main gotchas) — free the port before
|
||||
`desktop:dev`, or the window loads whatever else answers there.
|
||||
|
||||
## Where the app reaches
|
||||
|
||||
| Build variable | Default (desktop) | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `NUXT_PUBLIC_API_BASE` | `https://mathew.aranroig.com` | the wiki the app reads and writes; point it at `http://localhost:5000` to test against this machine |
|
||||
| `NUXT_PUBLIC_LEAN_URL` | empty | the Lean server `/lean` talks to; empty leaves that page saying there is no server here, which is the honest answer on a random laptop. The API's default CORS (`*`) carries the window's `tauri://localhost` origin fine — it sends a bearer header, not cookies |
|
||||
|
||||
Set them in the environment before `npm run desktop:build`.
|
||||
|
||||
## What the shell owns
|
||||
|
||||
- **one window per launch** — `tauri-plugin-single-instance`: pressing the
|
||||
icon again brings the open window forward instead of stacking a second wiki
|
||||
behind it (`src/lib.rs`).
|
||||
- **the way out** — `tauri-plugin-opener` is the door `app/utils/desktop.ts`
|
||||
hands outward links to the system's browser; `capabilities/default.json` is
|
||||
what grants it. That and `core:default` are the whole permission list:
|
||||
reading and writing the wiki is the webview's own business.
|
||||
- **no CSP** (`csp: null` in `tauri.conf.json`): the window loads no foreign
|
||||
script and talks to one API over HTTPS, and `/lean`'s Monaco wants
|
||||
`eval`-like tricks a strict `script-src` would have to excuse. The one
|
||||
script the page writes itself (the pre-paint theme, `public/preboot.js`)
|
||||
already lives in a **file**, so turning a policy on later is a config line —
|
||||
keep it that way if one is ever set.
|
||||
|
||||
Nothing here is signed: bundles come out unsigned, and updating is
|
||||
reinstalling. An updater, signing, and a per-machine "which wiki?" setting are
|
||||
the obvious next steps if this ever ships for real.
|
||||
|
||||
## Versioning
|
||||
|
||||
The app's version lives in `tauri.conf.json` (`version`); it names the
|
||||
installers and the platform bundle metadata. Commit `Cargo.lock` once a build
|
||||
has produced one — for an application binary, a locked dependency set is what
|
||||
makes a rebuild reproducible.
|
||||
3
frontend/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
7
frontend/src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "What the Mathew window may ask of the machine. Reading and writing the wiki is the webview's own business — plain HTTP out to the API and, if one is configured, a websocket to the Lean server — and needs no permission here. The one thing it cannot do by itself is hand a link to the system's browser, which is what `opener:default` lets app/utils/desktop.ts ask for.",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default", "opener:default"]
|
||||
}
|
||||
BIN
frontend/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
frontend/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
frontend/src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
frontend/src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
frontend/src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
frontend/src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
frontend/src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
frontend/src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
frontend/src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
frontend/src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
frontend/src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
frontend/src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
BIN
frontend/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
BIN
frontend/src-tauri/icons/icon.icns
Normal file
BIN
frontend/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
frontend/src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 951 B |
BIN
frontend/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
38
frontend/src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
/* Mathew as a window.
|
||||
|
||||
The shell is thin on purpose. Everything the app *is* — the open book, the
|
||||
editor, the five interface languages — is the Nuxt bundle it loads, written
|
||||
out by `npm run desktop:generate` and read from disk by the system webview
|
||||
(see src-tauri/README.md). The wiki reaches its own API by itself: no Rust
|
||||
command stands between the two, so an article is fetched the same way in the
|
||||
window as in a browser.
|
||||
|
||||
Two things are the shell's own:
|
||||
|
||||
- `opener`, the door a link goes out by. The wiki asks for it by name
|
||||
(app/utils/desktop.ts); `capabilities/default.json` is what answers.
|
||||
- one window per launch. Pressing the icon again brings the window that is
|
||||
already open to the front instead of stacking a second copy of the wiki
|
||||
behind it. */
|
||||
|
||||
#[cfg(desktop)]
|
||||
use tauri::Manager;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let mut builder = tauri::Builder::default().plugin(tauri_plugin_opener::init());
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
builder
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running Mathew");
|
||||
}
|
||||
7
frontend/src-tauri/src/main.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// An app window has no terminal to answer from: keep one from opening behind
|
||||
// it on Windows, where a release build would otherwise start a console.
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
mathew_desktop_lib::run()
|
||||
}
|
||||
47
frontend/src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Mathew",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.aranroig.mathew",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:3000",
|
||||
"beforeBuildCommand": "npm run desktop:generate",
|
||||
"frontendDist": "../dist-desktop/public"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"title": "Mathew",
|
||||
"width": 1440,
|
||||
"height": 900,
|
||||
"minWidth": 940,
|
||||
"minHeight": 600,
|
||||
"center": true,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"publisher": "Aran Roig",
|
||||
"homepage": "https://mathew.aranroig.com",
|
||||
"category": "Education",
|
||||
"shortDescription": "A collaborative wiki of mathematics",
|
||||
"longDescription": "Mathew is a wiki of mathematical concepts: Markdown with LaTeX, an open shelf of articles in several languages, a Markdown editor, a calculator, a graph plotter and a Lean 4 editor. This is the same wiki in an app window of its own; it reads and writes a Mathew wiki over HTTP, so it needs one to reach.",
|
||||
"copyright": "© Aran Roig",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||