Margin Calendar: a Google Calendar client for desktop and phone
Tauri 2, React 19 and zustand on the front, Rust behind. Rust owns auth, all HTTP to Google, the SQLite store, the sync loop, recurrence expansion and timezone maths. TypeScript owns rendering and never talks to Google, which keeps the content security policy locked to ipc:. Week, day and agenda views, and no month view: it would be a second layout engine, and the fit and fold logic that makes a day fit the window without scrolling is the whole point of the app. Runs on macOS, Linux, Android and iOS. Desktop catches Google's OAuth redirect on a loopback port. A phone cannot, and Google rejects loopback for mobile client types anyway, so it redirects to a custom URI scheme and needs its own public OAuth clients, which docs/mobile.md covers. Refresh tokens are sealed with XChaCha20-Poly1305 in the app data directory on every platform, with no OS credential store in the picture. On a phone the chrome becomes a top bar and a bottom tab bar, overlays become sheets, hover affordances become taps, and dragging out an event waits for a long press. Navigation moves one day at a time everywhere, a swipe included.
No files matched your search
@@ -0,0 +1,44 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Tauri build output
|
||||
src-tauri/target/
|
||||
src-tauri/gen/schemas/
|
||||
|
||||
# Xcode build output. The project under src-tauri/gen/apple is generated but committed, the
|
||||
# products of building it are not: Externals holds the static Rust library per architecture and
|
||||
# runs to several hundred MB.
|
||||
src-tauri/gen/apple/build/
|
||||
src-tauri/gen/apple/Externals/
|
||||
src-tauri/gen/apple/Pods/
|
||||
src-tauri/gen/apple/Podfile.lock
|
||||
xcuserdata/
|
||||
|
||||
# Google OAuth desktop client (real values, never commit)
|
||||
/google-credentials.json
|
||||
/client_secret_*.json
|
||||
|
||||
# Screenshots & Playwright MCP artifacts
|
||||
.playwright-mcp/
|
||||
/*.png
|
||||
@@ -0,0 +1,13 @@
|
||||
# Margin Calendar
|
||||
|
||||
A calendar for Google Calendar on macOS, Linux, Android and iOS. The grid owns the window, the day
|
||||
fits without scrolling, and on a desktop the whole thing is drivable from the keyboard.
|
||||
|
||||
It is a sibling to [margin](https://github.com/priyanshujain/margin) and shares its stack and its
|
||||
visual language.
|
||||
|
||||
Setup and the Google OAuth client it needs are in [docs/setup.md](docs/setup.md), and the extra
|
||||
clients a phone build needs are in [docs/mobile.md](docs/mobile.md). The product decisions are in
|
||||
[docs/design.md](docs/design.md), how it is built is in
|
||||
[docs/architecture.md](docs/architecture.md), and the conventions it follows are in
|
||||
[docs/conventions.md](docs/conventions.md).
|
||||
@@ -0,0 +1,133 @@
|
||||
# Architecture
|
||||
|
||||
Tauri 2, React 19, Vite, TypeScript and zustand on the front, Rust behind. Same stack as
|
||||
margin, which means the OAuth flow, the token handling and the build and bundle setup carry
|
||||
over rather than being invented again.
|
||||
|
||||
The split is strict. Rust owns authentication, all HTTP to Google, the local store, the sync
|
||||
loop, recurrence expansion and timezone arithmetic. TypeScript owns rendering and interaction
|
||||
and nothing else. The frontend never talks to Google, which keeps the content security policy
|
||||
locked to `ipc:` exactly as margin has it.
|
||||
|
||||
## Authentication
|
||||
|
||||
Lifted from `margin/src-tauri/src/gdrive.rs`, then split in two when mobile arrived. Both halves
|
||||
build the same consent URL with PKCE S256 and a CSRF state parameter and open it in the system
|
||||
browser through the opener plugin, never an in-app webview: Google blocks the embedded-webview
|
||||
flow, and it deserves to be blocked, because a webview the app controls can read the password
|
||||
typed into it. They differ only in how the answer comes back.
|
||||
|
||||
Desktop binds a listener on `127.0.0.1:0` and catches the redirect on the loopback socket. The
|
||||
verifier lives on that listener's stack for the two minutes it is alive.
|
||||
|
||||
Mobile cannot do that. iOS will not keep a background listener alive dependably, and Google
|
||||
rejects loopback redirects for Android and iOS client types outright, so the redirect is a custom
|
||||
URI scheme the OS routes back to the app through `tauri-plugin-deep-link`. There is no listener to
|
||||
hold the verifier, the browser is a separate app and this process may be backgrounded while the
|
||||
user consents, so the verifier waits in `AuthState.pending` and `handle_redirect` picks it up
|
||||
whenever the link lands. It is taken rather than read, so a replayed link cannot start a second
|
||||
exchange.
|
||||
|
||||
That split forces one more difference. A desktop client is confidential and has a secret; an
|
||||
Android or iOS client is public and has none, so `google-credentials.json` carries up to three
|
||||
clients and the build embeds the one for the platform it is compiling for. Details and the
|
||||
console steps are in [mobile.md](mobile.md).
|
||||
|
||||
The scope is `https://www.googleapis.com/auth/calendar` plus `openid email`. Calendar is a
|
||||
sensitive scope, so an unverified client shows the unverified-app interstitial and caps at 100
|
||||
users. Irrelevant for personal use, relevant the moment this ships the way margin ships.
|
||||
|
||||
Refresh tokens never go to disk in plaintext and never into SQLite. They are sealed with
|
||||
XChaCha20-Poly1305 in the app data directory, the same way on all five platforms.
|
||||
|
||||
There is no `keyring` here, and its absence is deliberate rather than an omission. It has no
|
||||
Android backend at all; on macOS it ties an item to the code signature, so an ad-hoc signed build
|
||||
gets a new identity on every compile and macOS re-asks for authorization every time; and on Linux
|
||||
it is missing on exactly the minimal window managers that run no Secret Service daemon. That left
|
||||
one real implementation behind four ways of reaching it. `src-tauri/src/google/secrets.rs` states
|
||||
plainly what the file is worth on each platform, which is more on the mobile sandboxes than on a
|
||||
desktop home directory.
|
||||
|
||||
## Store
|
||||
|
||||
SQLite through `rusqlite` with the bundled feature, so there is no system SQLite dependency on
|
||||
either platform.
|
||||
|
||||
Accounts hold an email and a reference naming the sealed token, never the token itself. The
|
||||
column is still called `keychain_ref` from when there was a keychain; renaming it would cost a
|
||||
migration and buy nothing. Calendars hold their account, summary,
|
||||
colour, selection state, access role, default timezone and their own sync token. Events hold
|
||||
the raw Google shape flattened: identity and etag, summary, description, location, start and
|
||||
end with their separate timezones, an all-day flag, the recurrence rule set, the pointer back
|
||||
to a master plus original start for exception instances, status, and attendees and conference
|
||||
data as JSON blobs. An outbox table holds pending writes.
|
||||
|
||||
Events are stored as Google returns them with `singleEvents=false`, meaning recurring series
|
||||
are one row carrying an RRULE rather than thousands of rows. Expansion happens on read.
|
||||
|
||||
## Sync
|
||||
|
||||
Incremental sync through `events.list` with a stored `syncToken`, one cursor per calendar,
|
||||
`singleEvents=false`, `showDeleted=true`, `maxResults=2500`. The parameter set has to be
|
||||
byte-identical across every call in a chain, including the initial one.
|
||||
|
||||
The constraint that shapes everything: `timeMin` and `timeMax` cannot be used with
|
||||
`syncToken`, along with `q`, `orderBy`, `updatedMin`, `iCalUID` and the extended property
|
||||
filters. There is no way to sync a window. The initial full sync pulls the entire calendar
|
||||
history, which for an old personal calendar is a few thousand rows and a one-time cost of
|
||||
seconds, and is only tolerable because recurring series collapse to single rows.
|
||||
|
||||
`nextSyncToken` appears only on the final page, so pages must be walked to exhaustion with
|
||||
`pageToken` before anything is persisted. A 410 means the token is dead: drop that calendar's
|
||||
rows and its cursor and full-resync it alone, without touching the others. `calendarList.list`
|
||||
carries its own sync token for the set of calendars itself.
|
||||
|
||||
Polling, not webhooks. Push notifications need a publicly verified HTTPS callback and channels
|
||||
that expire every few days, neither of which a desktop app has. Sixty seconds while the window
|
||||
is focused, five minutes when it is not. An incremental poll with no changes is a single small
|
||||
request against a million-a-day quota.
|
||||
|
||||
## Writes
|
||||
|
||||
Optimistic. A write lands in SQLite, is marked dirty, is enqueued in the outbox and renders
|
||||
immediately; the push to Google happens behind it. Requests carry `If-Match` with the stored
|
||||
etag, and a 412 means someone else changed it first, so refetch and surface the conflict
|
||||
rather than clobbering. Offline writes queue and drain on reconnect. The outbox is the reason
|
||||
the app works on a plane and the reason it never spins.
|
||||
|
||||
## Recurrence
|
||||
|
||||
The `rrule` crate, which is built on `chrono-tz`. Expansion takes a window and a set of
|
||||
masters, generates occurrences, drops any occurrence whose original start matches a cancelled
|
||||
or overridden instance, then merges the override events back in at their moved times. The
|
||||
frontend asks for a range and receives a flat array of instances; it has no concept of a
|
||||
recurrence rule.
|
||||
|
||||
Editing a series is three distinct operations. Editing one instance resolves the real instance
|
||||
through `events.instances` and patches that, rather than constructing the instance id by hand.
|
||||
Editing this and all following truncates the master's rule with an UNTIL and creates a new
|
||||
master from the split point. Editing the whole series patches the master. All three are worth
|
||||
tests before they are worth UI.
|
||||
|
||||
## Time
|
||||
|
||||
Every event keeps its own timezone; the grid renders in the local zone. All-day events are
|
||||
date-only and must never be shifted into a zone, which is the single most common bug in this
|
||||
category of app and deserves a test that runs in a non-UTC zone. DST transitions mean a day is
|
||||
sometimes 23 or 25 hours long, which the vertical fit calculation has to accept rather than
|
||||
assume away.
|
||||
|
||||
## Platforms
|
||||
|
||||
macOS uses the overlay title bar with the header row padded for the traffic lights, as margin
|
||||
does. Linux has no traffic lights, so that padding is conditional. Linux builds need
|
||||
`libwebkit2gtk-4.1-dev` and ship as AppImage and deb.
|
||||
|
||||
## Order of work
|
||||
|
||||
The sync engine and the grid are the two hard things and neither is proven by the other, so
|
||||
the first milestone is authentication, sync and a read-only grid with correct vertical fit.
|
||||
Recurrence and timezone correctness come second, with tests, because everything after depends
|
||||
on the instance stream being right. Create, edit and delete with the outbox come third.
|
||||
Multiple accounts, the command palette and natural language parsing come last, since they are
|
||||
the parts that are pleasant rather than load-bearing.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Conventions
|
||||
|
||||
This project is a sibling to `../margin` and follows its conventions deliberately rather than
|
||||
inventing new ones. When something here is unclear, the answer is almost always "do what margin
|
||||
does", and the file to look at is named below.
|
||||
|
||||
## Rust
|
||||
|
||||
`Result<T, String>` everywhere. No `anyhow`, no custom error enum except `google::api::ApiError`,
|
||||
which exists only because the sync engine has to distinguish a 410 and a 412 from everything else.
|
||||
|
||||
DTOs crossing the IPC boundary live in `src-tauri/src/dto.rs` and are marked
|
||||
`#[serde(rename_all = "camelCase")]`. That file is the contract and is frozen: implementation
|
||||
modules add bodies, not fields. Its mirror is `src/ipc.ts`.
|
||||
|
||||
Read Google's responses through `google::auth::read_json`, which takes the body to a `String`
|
||||
first so the error payload survives into the message. Ported from `margin/src-tauri/src/gdrive.rs:286`.
|
||||
|
||||
Heavy synchronous work goes behind `#[tauri::command(async)]` on a synchronous fn, which is
|
||||
margin's `pdf.rs:90` trick for getting off the main thread without hand-writing `spawn_blocking`.
|
||||
|
||||
Comments are rare and explain why, never what. Match the density in `lib.rs`.
|
||||
|
||||
## TypeScript
|
||||
|
||||
One zustand store per domain in `src/store/`. No middleware. One selector call per field
|
||||
(`useThing((s) => s.field)`, never a destructured object), actions as inline arrow properties, and
|
||||
`set((s) => ...)` returning `{}` to no-op. See `margin/src/store/useBackup.ts`.
|
||||
|
||||
Async actions use a string phase union (`"idle" | "syncing" | "error"`), never boolean loading
|
||||
flags. Errors stringify with `String(e)` and surface as a toast.
|
||||
|
||||
Side effects that touch disk, the DOM or Tauri live in a sibling module, never inside the store.
|
||||
|
||||
The OAuth connect flow reuses the promise-holding-its-own-resolver pattern from
|
||||
`margin/src/store/useBackup.ts:70`: `connect()` returns a promise whose `resolve` is stashed in
|
||||
state for a later Tauri event to settle.
|
||||
|
||||
Typed IPC wrappers live in `src/api/`, one module per domain, one thin function per command. They
|
||||
are already written; add bodies to Rust, not new wrappers.
|
||||
|
||||
## CSS
|
||||
|
||||
Flat kebab-case class names, not BEM. State is a `data-*` attribute, never an `is-` class.
|
||||
|
||||
Every colour, radius and size goes through a token in `src/styles/tokens.css`. If a value is not
|
||||
in there, add a token rather than a literal.
|
||||
|
||||
Dark mode is `data-theme` on `<html>`, with both palettes defining an identical variable set.
|
||||
Never a media query for theme.
|
||||
|
||||
Transitions name explicit properties and use `var(--ease)`. Never `transition: all`.
|
||||
|
||||
Responsiveness is JS-driven through `useCompact()`, which writes `data-compact` on the root.
|
||||
Style against that attribute rather than adding media queries.
|
||||
|
||||
`usePhone()` and `useTouch()` write `data-phone` and `data-touch` the same way, and they answer
|
||||
different questions. `data-phone` is a window too narrow for the desktop chrome and it governs
|
||||
layout; `data-touch` is a coarse pointer and it governs interaction. A tablet is touch and not a
|
||||
phone, a narrow desktop window is a phone and not touch, and treating either as a proxy for the
|
||||
other is how a hover-only control ends up unreachable. Both are also set by the boot script in
|
||||
`index.html`, so the first paint is already the right shape.
|
||||
|
||||
A rule that reads "you cannot hover here" belongs on `data-touch`. A rule that reads "there is no
|
||||
room for this" belongs on `data-phone` or `data-compact`.
|
||||
|
||||
The one exception is the event block, which uses a container query on its own inline size. That is
|
||||
deliberate: `data-compact` describes the window, but what decides how many lines of a title fit is
|
||||
the block's own width, and in a three-deep overlap cluster that is a quarter of a column. Nothing
|
||||
else may reach for a container query without the same kind of reason.
|
||||
|
||||
Overlays follow margin's `.overlay` and `.panel` idiom, already in `app.css`, and every one of
|
||||
them registers with `useEscapeLayer` from `src/escape.ts` so Escape unwinds the layers in order.
|
||||
|
||||
## Icons
|
||||
|
||||
Inline Feather-style 24x24 stroke `d` strings passed to `<Icon d={...} />`. There is no icon set
|
||||
and no registry, and there will not be one. An icon-only button always carries a `title` with its
|
||||
shortcut written in real glyphs.
|
||||
|
||||
## Storage keys
|
||||
|
||||
Anything in `localStorage` is prefixed `margincal-`, following margin's convention in `theme.ts`
|
||||
and `panes.ts`. Keys read before first paint are restored by the blocking IIFE in `index.html`.
|
||||
|
||||
## Never
|
||||
|
||||
No CSS framework, no component library, no router, no zustand middleware, no directory trees in
|
||||
any document, and no em dashes anywhere including code comments.
|
||||
@@ -0,0 +1,135 @@
|
||||
# Design
|
||||
|
||||
A desktop calendar for Google Calendar, macOS and Linux. It exists because Google Calendar
|
||||
wastes vertical space you cannot reclaim and surrounds the grid with chrome you never asked
|
||||
for. Everything below follows from those two complaints.
|
||||
|
||||
## The grid owns the window
|
||||
|
||||
There is no permanent sidebar. The mini-month, the calendar list, search and settings are all
|
||||
overlays: summoned by a key or a hover, dismissed with Escape, never resident. The only
|
||||
persistent chrome is a single header row carrying the date range, the view switcher and the
|
||||
current time range. On macOS the traffic lights float over that row, so it costs no extra
|
||||
height.
|
||||
|
||||
This is the whole point. A 1440p monitor should show a week of your life, not a week of your
|
||||
life inside a browser inside a Google header.
|
||||
|
||||
## Vertical fit
|
||||
|
||||
The grid never scrolls. Row height is derived from the window, so whatever range is visible
|
||||
always fits exactly.
|
||||
|
||||
The visible range is computed from the events in the span you are looking at: floor to the
|
||||
hour before your earliest event, ceil to the hour after your latest, clamped to a minimum of
|
||||
eight hours so a quiet week does not render four enormous rows. Empty bands at the top and
|
||||
bottom fold into thin strips labelled with their range and a count of anything hiding inside.
|
||||
Click or press `z` to unfold one.
|
||||
|
||||
The range expands immediately when it needs to, but only contracts when it would shrink by two
|
||||
hours or more. Without that hysteresis the axis flickers as you page through weeks, and a
|
||||
flickering axis destroys the positional memory that makes a keyboard-driven calendar fast.
|
||||
|
||||
Interior gaps stay at full scale. A three-hour hole on a Wednesday afternoon is the most
|
||||
useful thing on the screen, because it is where work goes, and folding it automatically would
|
||||
make a packed day look identical to an open one. But you can fold one deliberately with `z`,
|
||||
and it stays folded across navigation until you unfold it. The fold is per range, remembered
|
||||
in local state, not derived from the data.
|
||||
|
||||
One consequence worth stating plainly: within the unfolded region the scale is strictly
|
||||
linear, so a block twice as tall is an event twice as long, always. That property is why the
|
||||
grid is worth having at all, and it is the thing automatic gap-folding would have cost.
|
||||
|
||||
## Events on the grid
|
||||
|
||||
Overlapping events use the standard constraint model. Sort by start, partition into collision
|
||||
clusters, assign each event the leftmost free column within its cluster, then let every event
|
||||
expand rightward into columns that stay free for its whole duration. Colliding events end up
|
||||
the same width, nothing visually overlaps, and an event that only collides briefly still gets
|
||||
most of the day's width.
|
||||
|
||||
An event block is a washed neutral surface with a two-pixel coloured edge on the left
|
||||
identifying its calendar. Colour never fills the block. This keeps the grid quiet enough to
|
||||
read as a shape while still letting you tell work from personal at a glance, and it means the
|
||||
app looks like a sibling of margin rather than a different product.
|
||||
|
||||
All-day events sit in a fixed band under the day headers, one row tall, with anything that does
|
||||
not fit behind a count that expands over the grid. The band's height has to be constant: the axis
|
||||
below is solved from whatever height is left, so a band that grew with its contents would change
|
||||
the row height as you paged and slide every hour on the grid, which is the same positional memory
|
||||
the contraction hysteresis exists to protect. A taller fixed reserve would be the other sin, an
|
||||
empty band eating space you cannot reclaim.
|
||||
|
||||
## Keyboard
|
||||
|
||||
Keyboard-first, mouse fully supported. Drag on empty grid to create, drag a block to move it,
|
||||
drag its edge to resize. None of that is second class.
|
||||
|
||||
Navigation is `h` and `l` for previous and next day, `H` and `L` for week, `t` for today.
|
||||
`j` and `k` move the selection through the events of the focused day. `d`, `w` and `a` switch
|
||||
view, and `m` summons the mini-month, since there is no month view for it to switch to. `z`
|
||||
folds or unfolds the band under the cursor.
|
||||
|
||||
Actions are `c` to create, `Enter` to open the selection, `e` to edit, `x` to delete, `/` to
|
||||
search, `Escape` to dismiss whatever is open. `Cmd-K` opens the command palette, which is also
|
||||
where creation happens.
|
||||
|
||||
Every binding is listed in an overlay behind `?`. Nothing is modal and nothing is chorded.
|
||||
Vimcal's ceiling is higher and its users find it confusing, which is a trade worth refusing.
|
||||
|
||||
## On a phone
|
||||
|
||||
Keyboard-first stops being a design when there is no keyboard, so a phone gets a different set of
|
||||
affordances for the same commands rather than a shrunken copy of the desktop ones. The build steps
|
||||
are in [mobile.md](mobile.md); the decisions are here.
|
||||
|
||||
The chrome inverts. The desktop header holds three groups across a single row and there is no
|
||||
arrangement of that which fits 390 points, so the date and the day arrows go to a top bar, the
|
||||
views go to a bottom tab bar where a thumb can reach them, and the rest goes into an overflow
|
||||
sheet. Overlays become bottom sheets for the same reason: a centred panel puts its buttons where
|
||||
the hand is not.
|
||||
|
||||
Interaction changes on touch, not on width. Anything that only appeared on hover is always visible
|
||||
instead, because a finger cannot hover and an affordance nobody can reach is not an affordance.
|
||||
Dragging out a new event waits for a long press, because the alternative is that every tap on an
|
||||
empty afternoon starts creating something. A tablet gets these and keeps the week grid; a narrow
|
||||
desktop window gets the layout and keeps its mouse behaviour.
|
||||
|
||||
Navigation still moves one day at a time. A swipe is one day, never a week.
|
||||
|
||||
The week view survives on a phone rather than being hidden, because an overview has value even at
|
||||
fifty points a column, but it is an overview: no times on the blocks, one letter for the day name,
|
||||
and tapping a day header drops into that day. The day view is what a phone opens on.
|
||||
|
||||
What does not change is the premise. The day still fits without scrolling, the grid still owns
|
||||
what is left after the two bars, and there is still no month view.
|
||||
|
||||
## Creating an event
|
||||
|
||||
Press `c` or `Cmd-K` and type. `lunch with sam tue 1pm 45m` creates a 45-minute event on
|
||||
Tuesday. The parse is shown live underneath the input as you type, as a plain sentence with
|
||||
the resolved date, time, duration and target calendar, so you can see what will be created
|
||||
before you commit. If the parse is ambiguous the preview says so rather than guessing
|
||||
silently.
|
||||
|
||||
`#work` targets a calendar, `at <place>` sets the location, a bare time range like `9-10am`
|
||||
sets both ends. Anything the parser does not recognise stays in the title.
|
||||
|
||||
Dragging on the grid also creates: drag the range, type the title in place, Enter. Same event,
|
||||
different mood.
|
||||
|
||||
## Meetings
|
||||
|
||||
Attendees and conference links are shown read-only on an event that has them, including who
|
||||
has accepted. There is no RSVP flow, no availability finder and no scheduling links. That is
|
||||
a deliberate cut, not an oversight, and it can be revisited once the calendar itself is good.
|
||||
|
||||
## Visual language
|
||||
|
||||
The token layer is lifted from margin unchanged: warm paper surfaces, ink and two softer ink
|
||||
tones, hairline borders, a four-step type scale, three radii, one easing curve, light and dark
|
||||
driven by `data-theme` on the root. The additions are grid-specific: hour and half-hour rule
|
||||
colours, the now-line, the folded-band surface, and a set of eight muted calendar hues chosen
|
||||
to sit on warm paper without shouting.
|
||||
|
||||
No CSS framework and no component library, same as margin. Hand-written CSS on top of tokens.
|
||||
@@ -0,0 +1,198 @@
|
||||
# Android and iOS
|
||||
|
||||
The same Rust core and the same React app, with a different shape of chrome and a different way of
|
||||
getting a token back from Google. Nothing is forked: the layout switches on a `data-phone`
|
||||
attribute and the OAuth flow switches on `cfg(mobile)`.
|
||||
|
||||
## Google will not let you reuse the desktop client
|
||||
|
||||
This is the part that cannot be automated away, so do it first or nothing will sign in.
|
||||
|
||||
Google issues OAuth clients per platform and refuses one in another's place. The desktop client
|
||||
redirects to a loopback port; Google rejects loopback redirects for Android and iOS client types
|
||||
outright, and iOS will not dependably keep a listener alive to catch one anyway. So a phone build
|
||||
redirects to a custom URI scheme instead, and needs its own client to do it.
|
||||
|
||||
Both mobile clients are **public**: no client secret exists, and PKCE is the only thing between an
|
||||
intercepted authorization code and a token. That is why the verifier is not optional anywhere in
|
||||
`auth.rs`.
|
||||
|
||||
In the Google Cloud console, on the same project that already has the Calendar API enabled:
|
||||
|
||||
1. Create an OAuth client of type **Android**. It wants the package name, which is
|
||||
`studio.margin.calendar`, and the SHA-1 fingerprint of the certificate that signs the build.
|
||||
For a debug build that is the shared debug keystore:
|
||||
|
||||
```
|
||||
keytool -list -v -keystore ~/.android/debug.keystore \
|
||||
-alias androiddebugkey -storepass android -keypass android
|
||||
```
|
||||
|
||||
A release build is signed with a different key and needs its fingerprint added too. An APK
|
||||
signed by a key Google has not been told about fails at consent, not at build.
|
||||
|
||||
2. Create an OAuth client of type **iOS**. It wants the bundle identifier, which is also
|
||||
`studio.margin.calendar`.
|
||||
|
||||
3. Put both client ids in `google-credentials.json` alongside the desktop one, matching
|
||||
`google-credentials.example.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"installed": { "client_id": "...", "client_secret": "...", "...": "..." },
|
||||
"android": { "client_id": "YOUR_ANDROID_CLIENT_ID.apps.googleusercontent.com" },
|
||||
"ios": { "client_id": "YOUR_IOS_CLIENT_ID.apps.googleusercontent.com" }
|
||||
}
|
||||
```
|
||||
|
||||
The file is gitignored and embedded at build time. A build with no `android` or `ios` block
|
||||
compiles and then says so at runtime rather than failing to compile, which is the same bargain
|
||||
the desktop client already had.
|
||||
|
||||
## The redirect schemes
|
||||
|
||||
Android redirects to `studio.margin.calendar:/oauth2redirect`. That is the package name, which is
|
||||
Google's documented form for an Android client, and being known at build time means it can sit in
|
||||
`AndroidManifest.xml` permanently rather than being pasted in per install.
|
||||
|
||||
iOS gets no such choice. Google requires the reversed client id, so the scheme is only knowable
|
||||
once your client id is: take the client id, drop the `.apps.googleusercontent.com` suffix, and
|
||||
prefix `com.googleusercontent.apps.`.
|
||||
|
||||
Register it in `src-tauri/tauri.conf.json`, as a second entry in the deep link plugin's scheme
|
||||
list:
|
||||
|
||||
```json
|
||||
"deep-link": {
|
||||
"desktop": { "schemes": ["studio.margin.calendar"] },
|
||||
"mobile": [{ "scheme": ["studio.margin.calendar", "com.googleusercontent.apps.YOUR_ID"] }]
|
||||
}
|
||||
```
|
||||
|
||||
Not in `Info.plist`. Editing that by hand looks like it works and then silently stops working: the
|
||||
plugin's build script rewrites `CFBundleURLTypes` wholesale from this config on every build, and
|
||||
when the `mobile` array is empty it deletes the key outright. That one empty array is why the
|
||||
callback was dead on both platforms at first, so if a deep link ever stops arriving, look here
|
||||
before anywhere else.
|
||||
|
||||
If the console shows you something different from either default, put it in the client's block in
|
||||
`google-credentials.json` as `redirect_uri` and it wins over both. Whatever you put there still
|
||||
has to have its scheme registered above, or the OS has no reason to hand the link to this app.
|
||||
|
||||
## Toolchain
|
||||
|
||||
iOS needs Xcode and the iOS Rust targets:
|
||||
|
||||
```
|
||||
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
|
||||
```
|
||||
|
||||
Android needs the SDK, an NDK of r25 or newer, a JDK that the generated Gradle build accepts (21
|
||||
works, 25 does not), and the four Android Rust targets:
|
||||
|
||||
```
|
||||
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
|
||||
export ANDROID_HOME=/opt/homebrew/share/android-commandlinetools
|
||||
export NDK_HOME=$ANDROID_HOME/ndk/27.2.12479018
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@21
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
pnpm tauri ios init # once, generates the Xcode project
|
||||
pnpm tauri ios dev # simulator, with the Vite dev server
|
||||
pnpm tauri ios build
|
||||
|
||||
pnpm tauri android init # once, generates the Gradle project
|
||||
pnpm tauri android dev # emulator or attached device
|
||||
pnpm tauri android build
|
||||
```
|
||||
|
||||
The generated projects are committed. Re-running `init` overwrites them, so check afterwards that
|
||||
`MainActivity.kt` still publishes the window insets, since nothing else on Android does.
|
||||
|
||||
Both `dev` commands start Vite themselves and fail outright if port 1430 is already taken, which
|
||||
it will be if a browser dev server is still up. Kill it, or pass a different port through
|
||||
`--config`.
|
||||
|
||||
Three failures that look like bugs and are not. Xcode refusing to build with "Entitlements file
|
||||
was modified during the build" is a stale mtime in DerivedData, fixed once by deleting
|
||||
`~/Library/Developer/Xcode/DerivedData/margin-calendar-*`. An Android run panicking with "failed
|
||||
to build WebSocket client, Connection refused" is a stale
|
||||
`$TMPDIR/studio.margin.calendar-server-addr` pointing at a dead port; delete it. And an emulator
|
||||
that will not boot by name usually means `avdmanager` and `emulator` disagree about where AVDs
|
||||
live, which `ANDROID_AVD_HOME` settles.
|
||||
|
||||
Finally, an empty calendar on a device is correct. The browser fixture is gated on not being
|
||||
inside Tauri, so on a phone the real backend answers and there is nothing to show until an account
|
||||
is connected. Events without signing in only ever happen in a browser.
|
||||
|
||||
## What is different on a phone
|
||||
|
||||
The desktop header carries three groups of controls across one row, which does not fit in 390
|
||||
points. Under `data-phone` it becomes a top bar with the date and the day arrows and a bottom tab
|
||||
bar with the views, and everything the trailing icon row used to hold moves into an overflow
|
||||
sheet. Overlays become bottom sheets. Both bars pad themselves out of the way of the notch and the
|
||||
home indicator with `env(safe-area-inset-*)`.
|
||||
|
||||
Under `data-touch`, which a tablet gets and a narrow desktop window does not, interaction changes
|
||||
rather than layout. Anything that only appeared on hover is always visible instead, because a
|
||||
finger cannot hover. Dragging out a new event waits for a long press, because on a touchscreen the
|
||||
alternative is that every tap on an empty afternoon starts creating something.
|
||||
|
||||
Navigation still moves one day at a time. A swipe is one day, not one week.
|
||||
|
||||
Both attributes are also set by the boot script in `index.html` before first paint, so a phone
|
||||
does not render the desktop layout for a frame and then jump.
|
||||
|
||||
## Keeping out from under the system bars
|
||||
|
||||
`--safe-top` and `--safe-bottom` are `env(safe-area-inset-*)` by default, which is correct on iOS
|
||||
and wrong on Android in a way that is easy to miss. Android's WebView derives those values from the
|
||||
display cutout alone and never from the system bars, so it reports 0 at the bottom while the
|
||||
navigation bar really occupies 24dp of gesture pill or 48dp of buttons, and the tab bar renders
|
||||
underneath it. `targetSdk` is 36, so edge-to-edge is mandatory and there is nothing to opt out of.
|
||||
The top only looked right on a test device by luck, because that device had a cutout; a phone
|
||||
without one would have tucked the top bar under the status bar for the same reason.
|
||||
|
||||
So Android measures the bars natively. `MainActivity.kt` reads `systemBars() or displayCutout()`
|
||||
from `WindowInsetsCompat`, exposes them over a JavaScript bridge, and re-fires on every inset
|
||||
change, which covers rotation, the keyboard, and switching between gesture and button navigation
|
||||
live. `src/safeArea.ts` converts device pixels to CSS pixels and writes the two variables onto the
|
||||
root, where they beat the `env()` defaults. Off Android the bridge is simply absent and the
|
||||
defaults stand, so iOS and desktop are untouched.
|
||||
|
||||
Two consequences worth knowing. `tauri android init` regenerates `MainActivity.kt`, and nothing
|
||||
else on Android supplies these values, so check the bridge is still there after re-running it.
|
||||
And installing the inset listener makes `env(safe-area-inset-*)` read 0 inside that WebView, which
|
||||
does not matter only because those two token lines are the sole consumers and the bridge overrides
|
||||
both with better numbers. Anything new that reaches for `env()` directly on Android will get zero.
|
||||
|
||||
iOS needed one line of native code for the same class of problem, in the opposite direction.
|
||||
|
||||
UIKit hands a scroll view the safe areas as content insets unless told otherwise, and wry never
|
||||
tells it otherwise: it touches the scroll view only to switch `bounces` off. WebKit then lays the
|
||||
page out in what is left. On an iPhone 17 Pro that meant a layout viewport 778pt tall against an
|
||||
874pt screen, still anchored at y 0, so the bottom 96pt of the display was outside the page
|
||||
altogether and showed as a dead band of shell colour under the tab bar. `body` is
|
||||
`position: fixed`, so nothing could paint down there whatever the stylesheet said.
|
||||
|
||||
`stop_uikit_shrinking_the_viewport` in `lib.rs` sets `contentInsetAdjustmentBehavior` to `never`
|
||||
through `with_webview`, which gives the page the whole screen back. The insets are not lost, they
|
||||
arrive as `env(safe-area-inset-*)` instead, which is where both bars already read them from.
|
||||
|
||||
Worth knowing before anyone tries to solve that in CSS, because it looks like a CSS problem and
|
||||
the obvious fix is worse than the bug. While the viewport was short, `100dvh` was the only unit
|
||||
reporting the real box; `vh`, `svh` and `lvh` all reported the full screen. Sizing the root
|
||||
`100svh` therefore did not reclaim the bottom of the screen, it laid the tab bar out past the clip
|
||||
where it was invisible and, less obviously, untappable. The root is `height: 100%`, which inherits
|
||||
whatever the box is and cannot drift if the native line ever regresses.
|
||||
|
||||
## Limits worth knowing
|
||||
|
||||
There is no auto-updater and no process restart on mobile: the store is the update channel, and
|
||||
both plugins are compiled out rather than merely hidden.
|
||||
|
||||
The initial sync pulls the whole calendar history, because Google forbids `timeMin` alongside a
|
||||
`syncToken`. That is already the desktop behaviour and it is slower on a phone radio.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Setup
|
||||
|
||||
## The Google OAuth client
|
||||
|
||||
The app talks to Google with its own OAuth desktop client, which is not in this repo and cannot
|
||||
be. You need to make one before the app can connect to anything.
|
||||
|
||||
1. In the Google Cloud console, enable the Google Calendar API on a project.
|
||||
2. On the OAuth consent screen, add the `https://www.googleapis.com/auth/calendar` scope. It is a
|
||||
sensitive scope, so an unverified client shows an interstitial and caps at 100 users. That is
|
||||
fine for personal use and matters the day this ships more widely.
|
||||
3. Create an OAuth client ID of type **Desktop app**. That type permits the loopback redirect, and
|
||||
Google allows installed clients any loopback port, so there is nothing to register. This client
|
||||
is for macOS, Linux and Windows only; phones need their own, see [mobile.md](mobile.md).
|
||||
4. Save the downloaded JSON as `google-credentials.json` in the repo root. It should match the
|
||||
shape of `google-credentials.example.json`: an `installed` object with `client_id`,
|
||||
`client_secret`, `auth_uri` and `token_uri`.
|
||||
|
||||
The real file is gitignored. The build embeds it when it is present and embeds the example when it
|
||||
is not, so a fresh clone builds and then tells you at runtime that Google Calendar is not set up
|
||||
yet, rather than failing to compile.
|
||||
|
||||
Building for a phone needs a second and third OAuth client, because Google will not accept a
|
||||
desktop client from Android or iOS. That, and the toolchain, is in [mobile.md](mobile.md).
|
||||
|
||||
Refresh tokens never go to disk in plaintext and never into the SQLite database. They are sealed
|
||||
with XChaCha20-Poly1305 in the app data directory, identically on all five platforms, by
|
||||
`src-tauri/src/google/secrets.rs`. That file states what the encryption is worth where: on iOS and
|
||||
Android the app sandbox is the real boundary and this is defence behind it, while on a desktop
|
||||
anyone who can read your home directory can read the token. There is no OS credential store in the
|
||||
picture and nothing will ever prompt you for keychain access.
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
pnpm install
|
||||
pnpm tauri dev
|
||||
```
|
||||
|
||||
`pnpm test` runs the frontend suites, `cargo test` inside `src-tauri` runs the Rust ones. Both
|
||||
should be green before anything is considered done.
|
||||
|
||||
## Linux
|
||||
|
||||
Building needs `libwebkit2gtk-4.1-dev`, `libgtk-3-dev`, `librsvg2-dev`,
|
||||
`libayatana-appindicator3-dev` and `libxdo-dev`. Build on the oldest baseline you intend to
|
||||
support, which is Ubuntu 22.04 or Debian 12, because the resulting binary will not run on anything
|
||||
older than the glibc it was linked against.
|
||||
|
||||
## Where the data lives
|
||||
|
||||
`~/Library/Application Support/studio.margin.calendar/` on macOS and
|
||||
`~/.local/share/studio.margin.calendar/` on Linux. The bundle identifier is deliberately distinct
|
||||
from margin's, so the two apps never share a directory. Deleting that directory resets the app;
|
||||
the accounts reconnect and sync pulls everything back.
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"installed": {
|
||||
"client_id": "YOUR_CLIENT_ID.apps.googleusercontent.com",
|
||||
"project_id": "your-project-id",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_secret": "YOUR_CLIENT_SECRET",
|
||||
"redirect_uris": ["http://127.0.0.1"]
|
||||
},
|
||||
|
||||
"android": {
|
||||
"client_id": "YOUR_ANDROID_CLIENT_ID.apps.googleusercontent.com"
|
||||
},
|
||||
|
||||
"ios": {
|
||||
"client_id": "YOUR_IOS_CLIENT_ID.apps.googleusercontent.com"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/margincal-mark.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var root = document.documentElement;
|
||||
|
||||
var t = localStorage.getItem("margincal-theme");
|
||||
if (t !== "light" && t !== "dark")
|
||||
t = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
root.setAttribute("data-theme", t);
|
||||
|
||||
// Both attributes are set again by usePhone/useTouch once React is up. They are set here
|
||||
// as well so the first paint is already the right shape: without it a phone renders the
|
||||
// desktop header and a seven column week for a frame and then jumps.
|
||||
var phone = matchMedia("(max-width: 640px)").matches;
|
||||
if (phone) root.setAttribute("data-phone", "");
|
||||
if (matchMedia("(pointer: coarse)").matches) root.setAttribute("data-touch", "");
|
||||
|
||||
// A seven column week on a phone is seven columns of nothing legible, so a phone that
|
||||
// has never been told otherwise opens on the day. An explicit choice still wins.
|
||||
var v = localStorage.getItem("margincal-view");
|
||||
if (v !== "day" && v !== "agenda" && v !== "week") v = phone ? "day" : "week";
|
||||
root.setAttribute("data-view", v);
|
||||
|
||||
var b = JSON.parse(localStorage.getItem("margincal-bounds") || "null");
|
||||
if (b && typeof b.start === "number" && typeof b.end === "number" && b.end > b.start) {
|
||||
root.style.setProperty("--bound-start", String(b.start));
|
||||
root.style.setProperty("--bound-end", String(b.end));
|
||||
}
|
||||
|
||||
var f = JSON.parse(localStorage.getItem("margincal-folds") || "null");
|
||||
root.style.setProperty("--fold-count", String(Array.isArray(f) ? f.length : 0));
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
<title>Margin Calendar</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "margin-calendar",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:ui": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-process": "^2",
|
||||
"@tauri-apps/plugin-updater": "^2",
|
||||
"chrono-node": "^2.7.7",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.4",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// The end-to-end suite drives the real UI in a browser, where `src/ipc.ts` routes every command to
|
||||
// the dev fixture. One command starts everything: `pnpm test:ui` brings up Vite itself and reuses a
|
||||
// dev server that is already running.
|
||||
//
|
||||
// The viewport is fixed, because half of what these tests assert is geometry: block heights, the
|
||||
// solved row height, whether the axis moved. A test that measured a window of unknown size would
|
||||
// be asserting nothing.
|
||||
|
||||
import { defineConfig } from "@playwright/test";
|
||||
|
||||
const PORT = 1430;
|
||||
const BASE_URL = `http://localhost:${PORT}`;
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
fullyParallel: true,
|
||||
// No retries on purpose. A test that only passes on the second go is a test that is lying about
|
||||
// something, and this suite exists because things that looked fine were not.
|
||||
retries: 0,
|
||||
reporter: [["list"]],
|
||||
// Traces and failure screenshots go somewhere already ignored, so a run cannot leave anything
|
||||
// behind for the next `git add` to pick up. The paths are printed with the failure that made
|
||||
// them.
|
||||
outputDir: "node_modules/.cache/playwright",
|
||||
timeout: 30_000,
|
||||
expect: { timeout: 5_000 },
|
||||
|
||||
use: {
|
||||
baseURL: BASE_URL,
|
||||
viewport: { width: 1440, height: 900 },
|
||||
deviceScaleFactor: 1,
|
||||
// The fixture is anchored to the browser's local day, so pinning the zone keeps a run on one
|
||||
// machine comparable with a run on another. Nothing in the suite depends on the zone itself.
|
||||
timezoneId: "Asia/Kolkata",
|
||||
locale: "en-GB",
|
||||
trace: "retain-on-failure",
|
||||
screenshot: "only-on-failure",
|
||||
},
|
||||
|
||||
// Deliberately not `devices["Desktop Chrome"]`: that device pins a Windows user agent, and the
|
||||
// keymap reads the platform off the user agent to decide whether the primary modifier is Command
|
||||
// or Control. A faked platform would test the wrong half of every shortcut.
|
||||
projects: [{ name: "chromium", use: { browserName: "chromium" } }],
|
||||
|
||||
webServer: {
|
||||
command: "pnpm dev",
|
||||
url: BASE_URL,
|
||||
reuseExistingServer: true,
|
||||
timeout: 60_000,
|
||||
stdout: "ignore",
|
||||
stderr: "pipe",
|
||||
},
|
||||
});
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
/target/
|
||||
@@ -0,0 +1,60 @@
|
||||
[package]
|
||||
name = "margin-calendar"
|
||||
version = "0.1.0"
|
||||
description = "A calendar for Google Calendar"
|
||||
authors = ["Margin"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "margin_calendar_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
# Mobile has no loopback listener to catch Google's redirect, so the OAuth answer comes back as a
|
||||
# custom URI scheme the OS routes to this app. Registered on desktop too, so both flows are one
|
||||
# code path with one difference in it rather than two.
|
||||
tauri-plugin-deep-link = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
base64 = "0.22"
|
||||
sha2 = "0.10"
|
||||
rand = "0.8"
|
||||
url = "2"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
|
||||
rusqlite = { version = "0.37", features = ["bundled"] }
|
||||
rrule = "0.14"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
chrono-tz = "0.10"
|
||||
tokio = { version = "1", features = ["sync", "time"] }
|
||||
|
||||
# Refresh tokens are sealed with XChaCha20-Poly1305 and kept in the app data directory. There is no
|
||||
# `keyring` here on purpose: it has no Android backend at all, and on macOS it ties the item to the
|
||||
# code signature, so every rebuild re-prompts for authorization. src/google/secrets.rs states what
|
||||
# the file is and is not worth on each platform.
|
||||
chacha20poly1305 = "0.10"
|
||||
|
||||
# There is no auto-updater and no process to restart on a phone: the store is the update channel.
|
||||
# Gated here as well as behind cfg(desktop) in lib.rs so a mobile build does not compile them at all.
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
|
||||
# One UIKit property that wry does not set for us; stop_uikit_shrinking_the_viewport in lib.rs says
|
||||
# which one and why. These versions are the ones wry already resolves for its own iOS backend, so
|
||||
# matching them keeps a single copy of objc2 in the build rather than a second incompatible one.
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
objc2 = "0.6"
|
||||
objc2-ui-kit = { version = "0.3", default-features = false, features = [
|
||||
"std",
|
||||
"UIResponder",
|
||||
"UIView",
|
||||
"UIScrollView",
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
@@ -0,0 +1,29 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
embed_credentials();
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
// The OAuth desktop client is baked into the binary, but a clone of this repo has no
|
||||
// google-credentials.json. Embedding the example file instead of failing the build turns a
|
||||
// confusing compile error into the runtime "not set up yet" message from load_credentials.
|
||||
fn embed_credentials() {
|
||||
let root: PathBuf = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.expect("manifest dir has a parent")
|
||||
.to_path_buf();
|
||||
let real = root.join("google-credentials.json");
|
||||
let example = root.join("google-credentials.example.json");
|
||||
|
||||
println!("cargo:rerun-if-changed={}", real.display());
|
||||
println!("cargo:rerun-if-changed={}", example.display());
|
||||
|
||||
let source = if real.exists() { &real } else { &example };
|
||||
let contents = std::fs::read(source)
|
||||
.unwrap_or_else(|e| panic!("could not read {}: {e}", source.display()));
|
||||
|
||||
let out = Path::new(&std::env::var("OUT_DIR").expect("OUT_DIR is set"))
|
||||
.join("google-credentials.json");
|
||||
std::fs::write(&out, contents).expect("could not write embedded credentials");
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window, on every platform",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default", "opener:default", "deep-link:default"]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "desktop",
|
||||
"description": "Window control, the updater and process restart, none of which a phone has",
|
||||
"platforms": ["macOS", "windows", "linux"],
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:window:allow-destroy",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"updater:default",
|
||||
"process:allow-restart"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
@@ -0,0 +1,20 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
key.properties
|
||||
keystore.properties
|
||||
|
||||
/.tauri
|
||||
/tauri.settings.gradle
|
||||
@@ -0,0 +1,6 @@
|
||||
/src/main/**/generated
|
||||
/src/main/jniLibs/**/*.so
|
||||
/src/main/assets/tauri.conf.json
|
||||
/tauri.build.gradle.kts
|
||||
/proguard-tauri.pro
|
||||
/tauri.properties
|
||||
@@ -0,0 +1,71 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("rust")
|
||||
}
|
||||
|
||||
val tauriProperties = Properties().apply {
|
||||
val propFile = file("tauri.properties")
|
||||
if (propFile.exists()) {
|
||||
propFile.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 36
|
||||
namespace = "studio.margin.calendar"
|
||||
defaultConfig {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||
applicationId = "studio.margin.calendar"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||
}
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
isMinifyEnabled = false
|
||||
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
|
||||
}
|
||||
}
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
*fileTree(".") { include("**/*.pro") }
|
||||
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
.toList().toTypedArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
rust {
|
||||
rootDirRel = "../../../"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.webkit:webkit:1.14.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("androidx.activity:activity-ktx:1.10.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("androidx.lifecycle:lifecycle-process:2.10.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||
}
|
||||
|
||||
apply(from = "tauri.build.gradle.kts")
|
||||
@@ -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
|
||||
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!-- AndroidTV support -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.margin_calendar"
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/main_activity_title"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<!-- AndroidTV support -->
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- The OAuth callback filter below is written from plugins.deep-link.mobile in
|
||||
tauri.conf.json by the plugin's build.rs. Edit it there, not here: the same config
|
||||
is what DeepLinkPlugin.kt matches URLs against before it forwards them to Rust. -->
|
||||
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
|
||||
<intent-filter >
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<!-- ChromeOS ARC++ uses a different action for deep links -->
|
||||
<action android:name="org.chromium.arc.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="studio.margin.calendar" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</intent-filter>
|
||||
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
|
||||
</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" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,60 @@
|
||||
package studio.margin.calendar
|
||||
|
||||
import android.os.Bundle
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
|
||||
/**
|
||||
* Android's WebView works out env(safe-area-inset-*) from the display cutout and from nothing else,
|
||||
* so the status and navigation bars overlap the page while env() still reads 0 underneath them. A
|
||||
* targetSdk of 36 makes edge to edge mandatory, so there is no overlap to opt out of: the only way
|
||||
* out is to measure the bars and tell the page. That is what this does, and the page turns the
|
||||
* numbers into --safe-top and --safe-bottom (src/safeArea.ts).
|
||||
*/
|
||||
class MainActivity : TauriActivity() {
|
||||
// Device pixels. Written on the UI thread by the insets listener and read on the WebView's
|
||||
// JavaScript bridge thread, which is a different one.
|
||||
@Volatile private var topPx = 0
|
||||
@Volatile private var bottomPx = 0
|
||||
|
||||
inner class SafeArea {
|
||||
@JavascriptInterface fun top(): Int = topPx
|
||||
|
||||
@JavascriptInterface fun bottom(): Int = bottomPx
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onWebViewCreate(webView: WebView) {
|
||||
// wry calls this between constructing the WebView and loading the URL, which is the only point
|
||||
// at which an interface can be added and still be there for the first document.
|
||||
webView.addJavascriptInterface(SafeArea(), "__androidSafeArea")
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(webView) { view, insets ->
|
||||
// The cutout is folded in rather than trusted on its own: a landscape cutout down one side
|
||||
// is not a system bar, and a hidden system bar is not a cutout, and the page has to clear
|
||||
// whichever of the two is deeper.
|
||||
val bars =
|
||||
insets.getInsets(
|
||||
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
|
||||
)
|
||||
topPx = bars.top
|
||||
bottomPx = bars.bottom
|
||||
// The page reads the bridge itself as it boots. This is for everything after that: a
|
||||
// rotation, or a switch between gesture and three button navigation, moves both numbers.
|
||||
webView.evaluateJavascript("window.dispatchEvent(new Event('androidsafearea'))", null)
|
||||
// Handed on rather than returned, because a listener stands in for the view's own handler
|
||||
// instead of running alongside it, and the WebView would otherwise see no insets at all.
|
||||
// It does not bring env(safe-area-inset-*) back on its own: that reads 0 here whatever we
|
||||
// do, which is the whole reason the page takes its numbers from the bridge above.
|
||||
ViewCompat.onApplyWindowInsets(view, insets)
|
||||
}
|
||||
ViewCompat.requestApplyInsets(webView)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
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="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -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:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Hello World!"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,6 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.margin_calendar" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<string name="app_name">"Margin Calendar"</string>
|
||||
<string name="main_activity_title">"Margin Calendar"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.margin_calendar" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,22 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.0")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("clean").configure {
|
||||
delete("build")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
create("pluginsForCoolKids") {
|
||||
id = "rust"
|
||||
implementationClass = "RustPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(gradleApi())
|
||||
implementation("com.android.tools.build:gradle:8.11.0")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import java.io.File
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.logging.LogLevel
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
open class BuildTask : DefaultTask() {
|
||||
@Input
|
||||
var rootDirRel: String? = null
|
||||
@Input
|
||||
var target: String? = null
|
||||
@Input
|
||||
var release: Boolean? = null
|
||||
|
||||
@TaskAction
|
||||
fun assemble() {
|
||||
val executable = """pnpm""";
|
||||
try {
|
||||
runTauriCli(executable)
|
||||
} catch (e: Exception) {
|
||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
// Try different Windows-specific extensions
|
||||
val fallbacks = listOf(
|
||||
"$executable.exe",
|
||||
"$executable.cmd",
|
||||
"$executable.bat",
|
||||
)
|
||||
|
||||
var lastException: Exception = e
|
||||
for (fallback in fallbacks) {
|
||||
try {
|
||||
runTauriCli(fallback)
|
||||
return
|
||||
} catch (fallbackException: Exception) {
|
||||
lastException = fallbackException
|
||||
}
|
||||
}
|
||||
throw lastException
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun runTauriCli(executable: String) {
|
||||
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
|
||||
val target = target ?: throw GradleException("target cannot be null")
|
||||
val release = release ?: throw GradleException("release cannot be null")
|
||||
val args = listOf("tauri", "android", "android-studio-script");
|
||||
|
||||
project.exec {
|
||||
workingDir(File(project.projectDir, rootDirRel))
|
||||
executable(executable)
|
||||
args(args)
|
||||
if (project.logger.isEnabled(LogLevel.DEBUG)) {
|
||||
args("-vv")
|
||||
} else if (project.logger.isEnabled(LogLevel.INFO)) {
|
||||
args("-v")
|
||||
}
|
||||
if (release) {
|
||||
args("--release")
|
||||
}
|
||||
args(listOf("--target", target))
|
||||
}.assertNormalExitValue()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.get
|
||||
|
||||
const val TASK_GROUP = "rust"
|
||||
|
||||
open class Config {
|
||||
lateinit var rootDirRel: String
|
||||
}
|
||||
|
||||
open class RustPlugin : Plugin<Project> {
|
||||
private lateinit var config: Config
|
||||
|
||||
override fun apply(project: Project) = with(project) {
|
||||
config = extensions.create("rust", Config::class.java)
|
||||
|
||||
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
|
||||
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
|
||||
|
||||
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
|
||||
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
|
||||
|
||||
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
|
||||
|
||||
extensions.configure<ApplicationExtension> {
|
||||
@Suppress("UnstableApiUsage")
|
||||
flavorDimensions.add("abi")
|
||||
productFlavors {
|
||||
create("universal") {
|
||||
dimension = "abi"
|
||||
ndk {
|
||||
abiFilters += abiList
|
||||
}
|
||||
}
|
||||
defaultArchList.forEachIndexed { index, arch ->
|
||||
create(arch) {
|
||||
dimension = "abi"
|
||||
ndk {
|
||||
abiFilters.add(defaultAbiList[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
for (profile in listOf("debug", "release")) {
|
||||
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
|
||||
val buildTask = tasks.maybeCreate(
|
||||
"rustBuildUniversal$profileCapitalized",
|
||||
DefaultTask::class.java
|
||||
).apply {
|
||||
group = TASK_GROUP
|
||||
description = "Build dynamic library in $profile mode for all targets"
|
||||
}
|
||||
|
||||
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
|
||||
|
||||
for (targetPair in targetsList.withIndex()) {
|
||||
val targetName = targetPair.value
|
||||
val targetArch = archList[targetPair.index]
|
||||
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
|
||||
val targetBuildTask = project.tasks.maybeCreate(
|
||||
"rustBuild$targetArchCapitalized$profileCapitalized",
|
||||
BuildTask::class.java
|
||||
).apply {
|
||||
group = TASK_GROUP
|
||||
description = "Build dynamic library in $profile mode for $targetArch"
|
||||
rootDirRel = config.rootDirRel
|
||||
target = targetName
|
||||
release = profile == "release"
|
||||
}
|
||||
|
||||
buildTask.dependsOn(targetBuildTask)
|
||||
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
|
||||
targetBuildTask
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# 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=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# 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
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=false
|
||||
@@ -0,0 +1,6 @@
|
||||
#Tue May 10 19:22:52 CST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or 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.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# 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"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 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
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# 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"
|
||||
which java >/dev/null 2>&1 || 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
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,89 @@
|
||||
@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
|
||||
|
||||
@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=.
|
||||
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%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="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!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,3 @@
|
||||
include ':app'
|
||||
|
||||
apply from: 'tauri.settings.gradle'
|
||||
@@ -0,0 +1,3 @@
|
||||
xcuserdata/
|
||||
build/
|
||||
Externals/
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "[email protected]",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>method</key>
|
||||
<string>debugging</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17150" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17122"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="s0d-6b-0kx">
|
||||
<objects>
|
||||
<viewController id="Y6W-OH-hqX" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,21 @@
|
||||
# Uncomment the next line to define a global platform for your project
|
||||
|
||||
target 'margin-calendar_iOS' do
|
||||
platform :ios, '14.0'
|
||||
# Pods for margin-calendar_iOS
|
||||
end
|
||||
|
||||
target 'margin-calendar_macOS' do
|
||||
platform :osx, '11.0'
|
||||
# Pods for margin-calendar_macOS
|
||||
end
|
||||
|
||||
# Delete the deployment target for iOS and macOS, causing it to be inherited from the Podfile
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET'
|
||||
config.build_settings.delete 'MACOSX_DEPLOYMENT_TARGET'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
namespace ffi {
|
||||
extern "C" {
|
||||
void start_app();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#include "bindings/bindings.h"
|
||||
|
||||
int main(int argc, char * argv[]) {
|
||||
ffi::start_app();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
027A8ABDBEF8BBD53B7567E0 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE35A9DF9BD78C3583E7FC5A /* CoreGraphics.framework */; };
|
||||
301C79BAC9680139BDA2B1B9 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481146F91F640D48193D38B /* Metal.framework */; };
|
||||
6EBA510B4387F78D53CD3E07 /* libapp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AE625022A492F5C76B797AC2 /* libapp.a */; };
|
||||
742DD5E8169EE4583B151E73 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DA270EEE52796CFD8EC281A /* Security.framework */; };
|
||||
7E481312148309B8AA94B95C /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = 65E80ECEB0844651FB03158B /* main.mm */; };
|
||||
89071E2C472ACC0F43105CEA /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9E2CB1CFED1CED2BECCBB94 /* QuartzCore.framework */; };
|
||||
9191DAC879BEE67BC864E01D /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C57E62E8D422566DF7D919FD /* WebKit.framework */; };
|
||||
92277B81E1BC594DEDA75316 /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 245C8245C8B384E2FF25C48B /* MetalKit.framework */; };
|
||||
CC28802C3A0B94C1DDA4C5C4 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 8879346700D62B7DDE676FB9 /* assets */; };
|
||||
D3611CD2631DCF0504874997 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 42AC5D9341EC41579150A559 /* LaunchScreen.storyboard */; };
|
||||
ECFE37538824122085E847BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 463516468F6D0DFDE911E397 /* Assets.xcassets */; };
|
||||
F93C0964D01061FFB2C16E2B /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4B92AAC82F707F343DF3F9F /* UIKit.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
00DFE6DD99BE0C3AEAB97944 /* recur.rs */ = {isa = PBXFileReference; path = recur.rs; sourceTree = "<group>"; };
|
||||
0481146F91F640D48193D38B /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
|
||||
04B2978EA3C716D6EB74712D /* transport.rs */ = {isa = PBXFileReference; path = transport.rs; sourceTree = "<group>"; };
|
||||
06C647F836A02A8532C617DA /* read.rs */ = {isa = PBXFileReference; path = read.rs; sourceTree = "<group>"; };
|
||||
11EB0C96B4964DD8EB696D63 /* api.rs */ = {isa = PBXFileReference; path = api.rs; sourceTree = "<group>"; };
|
||||
1578DA0D4DFD83FCFC7CD967 /* bindings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bindings.h; sourceTree = "<group>"; };
|
||||
19E9713B598286F9A58F6524 /* auth.rs */ = {isa = PBXFileReference; path = auth.rs; sourceTree = "<group>"; };
|
||||
245C8245C8B384E2FF25C48B /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; };
|
||||
42975F72473A262C8DC12B2D /* model.rs */ = {isa = PBXFileReference; path = model.rs; sourceTree = "<group>"; };
|
||||
42AC5D9341EC41579150A559 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
463516468F6D0DFDE911E397 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
482B1BACC9A878BD6000F332 /* tests.rs */ = {isa = PBXFileReference; path = tests.rs; sourceTree = "<group>"; };
|
||||
5F2BD57CDF2418F2114AC00F /* mod.rs */ = {isa = PBXFileReference; path = mod.rs; sourceTree = "<group>"; };
|
||||
601DA7E7CE0314ED76601AB8 /* library.rs */ = {isa = PBXFileReference; path = library.rs; sourceTree = "<group>"; };
|
||||
63440E6A8AA7263282FF0880 /* schema.rs */ = {isa = PBXFileReference; path = schema.rs; sourceTree = "<group>"; };
|
||||
65E80ECEB0844651FB03158B /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = "<group>"; };
|
||||
6DA270EEE52796CFD8EC281A /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
|
||||
75B8603218CF087545CA23FE /* margin-calendar_iOS.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = "margin-calendar_iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
7C24690CAAB5FC9E1015052A /* main.rs */ = {isa = PBXFileReference; path = main.rs; sourceTree = "<group>"; };
|
||||
7DF7225718DEED80CC685D70 /* secrets.rs */ = {isa = PBXFileReference; path = secrets.rs; sourceTree = "<group>"; };
|
||||
83A2962170C125E12A298599 /* mod.rs */ = {isa = PBXFileReference; path = mod.rs; sourceTree = "<group>"; };
|
||||
8879346700D62B7DDE676FB9 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = SOURCE_ROOT; };
|
||||
9FDB21551CF4959451A03485 /* pull.rs */ = {isa = PBXFileReference; path = pull.rs; sourceTree = "<group>"; };
|
||||
A19747E3FB043C2FC441866B /* margin-calendar_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "margin-calendar_iOS.entitlements"; sourceTree = "<group>"; };
|
||||
A2B433E2EAB81F688739BD72 /* push.rs */ = {isa = PBXFileReference; path = push.rs; sourceTree = "<group>"; };
|
||||
A4B92AAC82F707F343DF3F9F /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
|
||||
AE625022A492F5C76B797AC2 /* libapp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libapp.a; sourceTree = "<group>"; };
|
||||
AF43A8B9F86EC0EE625D5AC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
BB370CF51946E1086F725448 /* dto.rs */ = {isa = PBXFileReference; path = dto.rs; sourceTree = "<group>"; };
|
||||
C2072E3C608C4C15356BFD25 /* sync.rs */ = {isa = PBXFileReference; path = sync.rs; sourceTree = "<group>"; };
|
||||
C57E62E8D422566DF7D919FD /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
|
||||
C9E2CB1CFED1CED2BECCBB94 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
|
||||
D903EBABA83CD7E1EFE1DAF5 /* write.rs */ = {isa = PBXFileReference; path = write.rs; sourceTree = "<group>"; };
|
||||
DE35A9DF9BD78C3583E7FC5A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
|
||||
E8E713A134B354F21DEF913B /* lib.rs */ = {isa = PBXFileReference; path = lib.rs; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
763B09C9EE617B71EF7F11D4 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6EBA510B4387F78D53CD3E07 /* libapp.a in Frameworks */,
|
||||
027A8ABDBEF8BBD53B7567E0 /* CoreGraphics.framework in Frameworks */,
|
||||
301C79BAC9680139BDA2B1B9 /* Metal.framework in Frameworks */,
|
||||
92277B81E1BC594DEDA75316 /* MetalKit.framework in Frameworks */,
|
||||
89071E2C472ACC0F43105CEA /* QuartzCore.framework in Frameworks */,
|
||||
742DD5E8169EE4583B151E73 /* Security.framework in Frameworks */,
|
||||
F93C0964D01061FFB2C16E2B /* UIKit.framework in Frameworks */,
|
||||
9191DAC879BEE67BC864E01D /* WebKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
08BDFB2F8332AB5AE4C89430 /* store */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
83A2962170C125E12A298599 /* mod.rs */,
|
||||
06C647F836A02A8532C617DA /* read.rs */,
|
||||
63440E6A8AA7263282FF0880 /* schema.rs */,
|
||||
D903EBABA83CD7E1EFE1DAF5 /* write.rs */,
|
||||
);
|
||||
path = store;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1DFBF6D830071E5D1FE33720 /* margin-calendar_iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AF43A8B9F86EC0EE625D5AC0 /* Info.plist */,
|
||||
A19747E3FB043C2FC441866B /* margin-calendar_iOS.entitlements */,
|
||||
);
|
||||
path = "margin-calendar_iOS";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2717CB47409E376228915D92 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8879346700D62B7DDE676FB9 /* assets */,
|
||||
463516468F6D0DFDE911E397 /* Assets.xcassets */,
|
||||
42AC5D9341EC41579150A559 /* LaunchScreen.storyboard */,
|
||||
701F7017030A43C5B1F02E43 /* Externals */,
|
||||
1DFBF6D830071E5D1FE33720 /* margin-calendar_iOS */,
|
||||
F2D7B8DA6405BBC1A306E0F5 /* Sources */,
|
||||
C66369FCC7BD73AD6D6B329D /* src */,
|
||||
7B0CEBED082C2704D1AE2349 /* Frameworks */,
|
||||
FD095DBA118C3E02B31F8572 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
31E412A61C3C36BBEA22CAB4 /* sync */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
42975F72473A262C8DC12B2D /* model.rs */,
|
||||
9FDB21551CF4959451A03485 /* pull.rs */,
|
||||
A2B433E2EAB81F688739BD72 /* push.rs */,
|
||||
482B1BACC9A878BD6000F332 /* tests.rs */,
|
||||
04B2978EA3C716D6EB74712D /* transport.rs */,
|
||||
);
|
||||
path = sync;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
701F7017030A43C5B1F02E43 /* Externals */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = Externals;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7B0CEBED082C2704D1AE2349 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DE35A9DF9BD78C3583E7FC5A /* CoreGraphics.framework */,
|
||||
AE625022A492F5C76B797AC2 /* libapp.a */,
|
||||
0481146F91F640D48193D38B /* Metal.framework */,
|
||||
245C8245C8B384E2FF25C48B /* MetalKit.framework */,
|
||||
C9E2CB1CFED1CED2BECCBB94 /* QuartzCore.framework */,
|
||||
6DA270EEE52796CFD8EC281A /* Security.framework */,
|
||||
A4B92AAC82F707F343DF3F9F /* UIKit.framework */,
|
||||
C57E62E8D422566DF7D919FD /* WebKit.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
819B89450E4EF4F878332BE3 /* bindings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1578DA0D4DFD83FCFC7CD967 /* bindings.h */,
|
||||
);
|
||||
path = bindings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8465EC9E3B20D359B37ED5CC /* google */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
11EB0C96B4964DD8EB696D63 /* api.rs */,
|
||||
19E9713B598286F9A58F6524 /* auth.rs */,
|
||||
5F2BD57CDF2418F2114AC00F /* mod.rs */,
|
||||
7DF7225718DEED80CC685D70 /* secrets.rs */,
|
||||
);
|
||||
path = google;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C66369FCC7BD73AD6D6B329D /* src */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BB370CF51946E1086F725448 /* dto.rs */,
|
||||
E8E713A134B354F21DEF913B /* lib.rs */,
|
||||
601DA7E7CE0314ED76601AB8 /* library.rs */,
|
||||
7C24690CAAB5FC9E1015052A /* main.rs */,
|
||||
00DFE6DD99BE0C3AEAB97944 /* recur.rs */,
|
||||
C2072E3C608C4C15356BFD25 /* sync.rs */,
|
||||
8465EC9E3B20D359B37ED5CC /* google */,
|
||||
08BDFB2F8332AB5AE4C89430 /* store */,
|
||||
31E412A61C3C36BBEA22CAB4 /* sync */,
|
||||
);
|
||||
name = src;
|
||||
path = ../../src;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C7CA186F2513338D60A6B00E /* margin-calendar */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
65E80ECEB0844651FB03158B /* main.mm */,
|
||||
819B89450E4EF4F878332BE3 /* bindings */,
|
||||
);
|
||||
path = "margin-calendar";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F2D7B8DA6405BBC1A306E0F5 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C7CA186F2513338D60A6B00E /* margin-calendar */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD095DBA118C3E02B31F8572 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
75B8603218CF087545CA23FE /* margin-calendar_iOS.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
44343353A6DEDC69656FAA54 /* margin-calendar_iOS */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = E8ACAA0021FD841CDD504F34 /* Build configuration list for PBXNativeTarget "margin-calendar_iOS" */;
|
||||
buildPhases = (
|
||||
D2BB0B9B44B3A2E1C386BFAD /* Build Rust Code */,
|
||||
DEF597EC6F9C166B9E68C45C /* Sources */,
|
||||
066AEAE8FD82D64732953ED0 /* Resources */,
|
||||
763B09C9EE617B71EF7F11D4 /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "margin-calendar_iOS";
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = "margin-calendar_iOS";
|
||||
productReference = 75B8603218CF087545CA23FE /* margin-calendar_iOS.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
FBBBFD542B155C58C96B76B5 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1430;
|
||||
TargetAttributes = {
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 94C3379210AAB44F71F32C5E /* Build configuration list for PBXProject "margin-calendar" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
Base,
|
||||
en,
|
||||
);
|
||||
mainGroup = 2717CB47409E376228915D92;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = FD095DBA118C3E02B31F8572 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
44343353A6DEDC69656FAA54 /* margin-calendar_iOS */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
066AEAE8FD82D64732953ED0 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
ECFE37538824122085E847BB /* Assets.xcassets in Resources */,
|
||||
D3611CD2631DCF0504874997 /* LaunchScreen.storyboard in Resources */,
|
||||
CC28802C3A0B94C1DDA4C5C4 /* assets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
D2BB0B9B44B3A2E1C386BFAD /* Build Rust Code */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Build Rust Code";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a",
|
||||
"$(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "pnpm tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths \"${FRAMEWORK_SEARCH_PATHS:?}\" --header-search-paths \"${HEADER_SEARCH_PATHS:?}\" --gcc-preprocessor-definitions \"${GCC_PREPROCESSOR_DEFINITIONS:-}\" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
DEF597EC6F9C166B9E68C45C /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7E481312148309B8AA94B95C /* main.mm in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
68D6BB2FB85DAC1C8BAB279F /* debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"DEBUG=1",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = debug;
|
||||
};
|
||||
7E114EA455562314CA5392E8 /* release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = release;
|
||||
};
|
||||
C7BFE7ABB61BEFEA5FB5615D /* debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ARCHS = (
|
||||
arm64,
|
||||
);
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "margin-calendar_iOS/margin-calendar_iOS.entitlements";
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\".\"",
|
||||
);
|
||||
INFOPLIST_FILE = "margin-calendar_iOS/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = studio.margin.calendar;
|
||||
PRODUCT_NAME = "Margin Calendar";
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALID_ARCHS = arm64;
|
||||
};
|
||||
name = debug;
|
||||
};
|
||||
ED0652ADBD2F428F7C930D66 /* release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ARCHS = (
|
||||
arm64,
|
||||
);
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "margin-calendar_iOS/margin-calendar_iOS.entitlements";
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\".\"",
|
||||
);
|
||||
INFOPLIST_FILE = "margin-calendar_iOS/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = studio.margin.calendar;
|
||||
PRODUCT_NAME = "Margin Calendar";
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALID_ARCHS = arm64;
|
||||
};
|
||||
name = release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
94C3379210AAB44F71F32C5E /* Build configuration list for PBXProject "margin-calendar" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
68D6BB2FB85DAC1C8BAB279F /* debug */,
|
||||
7E114EA455562314CA5392E8 /* release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = debug;
|
||||
};
|
||||
E8ACAA0021FD841CDD504F34 /* Build configuration list for PBXNativeTarget "margin-calendar_iOS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C7BFE7ABB61BEFEA5FB5615D /* debug */,
|
||||
ED0652ADBD2F428F7C930D66 /* release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = debug;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = FBBBFD542B155C58C96B76B5 /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BuildSystemType</key>
|
||||
<string>Original</string>
|
||||
<key>DisableBuildSystemDeprecationDiagnostic</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,131 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
runPostActionsOnFailure = "NO">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "44343353A6DEDC69656FAA54"
|
||||
BuildableName = "margin-calendar_iOS.app"
|
||||
BlueprintName = "margin-calendar_iOS"
|
||||
ReferencedContainer = "container:margin-calendar.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "NO"
|
||||
onlyGenerateCoverageForSpecifiedTargets = "NO">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "44343353A6DEDC69656FAA54"
|
||||
BuildableName = "margin-calendar_iOS.app"
|
||||
BlueprintName = "margin-calendar_iOS"
|
||||
ReferencedContainer = "container:margin-calendar.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
</Testables>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_BACKTRACE"
|
||||
value = "full"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_LOG"
|
||||
value = "info"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "44343353A6DEDC69656FAA54"
|
||||
BuildableName = "margin-calendar_iOS.app"
|
||||
BlueprintName = "margin-calendar_iOS"
|
||||
ReferencedContainer = "container:margin-calendar.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_BACKTRACE"
|
||||
value = "full"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_LOG"
|
||||
value = "info"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "release"
|
||||
shouldUseLaunchSchemeArgsEnv = "NO"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "44343353A6DEDC69656FAA54"
|
||||
BuildableName = "margin-calendar_iOS.app"
|
||||
BlueprintName = "margin-calendar_iOS"
|
||||
ReferencedContainer = "container:margin-calendar.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_BACKTRACE"
|
||||
value = "full"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_LOG"
|
||||
value = "info"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
<string>metal</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>studio.margin.calendar</string>
|
||||
</array>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>studio.margin.calendar</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
@@ -0,0 +1,88 @@
|
||||
name: margin-calendar
|
||||
options:
|
||||
bundleIdPrefix: studio.margin.calendar
|
||||
deploymentTarget:
|
||||
iOS: 14.0
|
||||
fileGroups: [../../src]
|
||||
configs:
|
||||
debug: debug
|
||||
release: release
|
||||
settingGroups:
|
||||
app:
|
||||
base:
|
||||
PRODUCT_NAME: Margin Calendar
|
||||
PRODUCT_BUNDLE_IDENTIFIER: studio.margin.calendar
|
||||
targetTemplates:
|
||||
app:
|
||||
type: application
|
||||
sources:
|
||||
- path: Sources
|
||||
scheme:
|
||||
environmentVariables:
|
||||
RUST_BACKTRACE: full
|
||||
RUST_LOG: info
|
||||
settings:
|
||||
groups: [app]
|
||||
targets:
|
||||
margin-calendar_iOS:
|
||||
type: application
|
||||
platform: iOS
|
||||
sources:
|
||||
- path: Sources
|
||||
- path: Assets.xcassets
|
||||
- path: Externals
|
||||
- path: margin-calendar_iOS
|
||||
- path: assets
|
||||
buildPhase: resources
|
||||
type: folder
|
||||
- path: LaunchScreen.storyboard
|
||||
info:
|
||||
path: margin-calendar_iOS/Info.plist
|
||||
properties:
|
||||
LSRequiresIPhoneOS: true
|
||||
UILaunchStoryboardName: LaunchScreen
|
||||
UIRequiredDeviceCapabilities: [arm64, metal]
|
||||
UISupportedInterfaceOrientations:
|
||||
- UIInterfaceOrientationPortrait
|
||||
- UIInterfaceOrientationLandscapeLeft
|
||||
- UIInterfaceOrientationLandscapeRight
|
||||
UISupportedInterfaceOrientations~ipad:
|
||||
- UIInterfaceOrientationPortrait
|
||||
- UIInterfaceOrientationPortraitUpsideDown
|
||||
- UIInterfaceOrientationLandscapeLeft
|
||||
- UIInterfaceOrientationLandscapeRight
|
||||
CFBundleShortVersionString: 0.1.0
|
||||
CFBundleVersion: "0.1.0"
|
||||
entitlements:
|
||||
path: margin-calendar_iOS/margin-calendar_iOS.entitlements
|
||||
scheme:
|
||||
environmentVariables:
|
||||
RUST_BACKTRACE: full
|
||||
RUST_LOG: info
|
||||
settings:
|
||||
base:
|
||||
ENABLE_BITCODE: false
|
||||
ARCHS: [arm64]
|
||||
VALID_ARCHS: arm64
|
||||
LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)
|
||||
LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true
|
||||
EXCLUDED_ARCHS[sdk=iphoneos*]: x86_64
|
||||
groups: [app]
|
||||
dependencies:
|
||||
- framework: libapp.a
|
||||
embed: false
|
||||
- sdk: CoreGraphics.framework
|
||||
- sdk: Metal.framework
|
||||
- sdk: MetalKit.framework
|
||||
- sdk: QuartzCore.framework
|
||||
- sdk: Security.framework
|
||||
- sdk: UIKit.framework
|
||||
- sdk: WebKit.framework
|
||||
preBuildScripts:
|
||||
- script: pnpm tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths "${FRAMEWORK_SEARCH_PATHS:?}" --header-search-paths "${HEADER_SEARCH_PATHS:?}" --gcc-preprocessor-definitions "${GCC_PREPROCESSOR_DEFINITIONS:-}" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}
|
||||
name: Build Rust Code
|
||||
basedOnDependencyAnalysis: false
|
||||
outputFiles:
|
||||
- $(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a
|
||||
- $(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.1 KiB |