Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ if (
);
}

// Every variant uses Pip (scripts/export-android-icons.ts), told apart by name only. iOS
// Every variant uses Pip (scripts/export-android-icons.ts). iOS
// gets a full-bleed square because it applies its own mask; Android gets the
// mark alone over a navy adaptive background so the launcher mask never crops it.
const AGENTS_NAVY = "#111925";
Expand All @@ -49,14 +49,22 @@ const AGENTS_ASSETS = {
androidSplashIcon: "./assets/agents/android-splash-icon.png",
androidMonochromeIcon: "./assets/agents/android-icon-monochrome.png",
} as const;
// Dev builds match their in-app mark (src/components/brandAssets.ts): white Pip on the blueprint.
const DEV_ASSETS = {
...AGENTS_ASSETS,
appIcon: "./assets/agents/dev-ios-icon-1024.png",
iosIcon: "./assets/agents/dev-ios-icon-1024.png",
androidAdaptiveForeground: "./assets/agents/android-icon-monochrome.png",
androidAdaptiveBackgroundImage: "./assets/agents/android-dev-icon-background.png",
} as const;

const VARIANT_CONFIG = {
development: {
appName: `${APP_NAME} Dev`,
scheme: "t3code-personal-dev",
iosBundleIdentifier: "com.jayleaton.t3code.dev",
androidPackage: "com.jayleaton.t3code.dev",
assets: AGENTS_ASSETS,
assets: DEV_ASSETS,
},
preview: {
appName: `${APP_NAME} Preview`,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/mobile/assets/agents/dev-ios-icon-1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,12 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm
const [connectionExpanded, setConnectionExpanded] = useState(false);
const [pending, setPending] = useState<string | null>(null);
const pendingRef = useRef(false);
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
// Shown in the section of the action that produced it, beside its button.
const [feedback, setFeedback] = useState<{
readonly section: "app" | "providers";
readonly message: string;
readonly failed: boolean;
} | null>(null);
const [releaseCheck, checkRelease] = useAtom(environmentReleaseCheckAtom(environmentId), {
mode: "promiseExit",
});
Expand All @@ -102,20 +106,25 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm
// The server refuses checks and installs; say so instead of offering them.
const updatesDenial = managedUpdatesDenial(managedPolicy);

async function run(label: string, action: () => Promise<void>) {
async function run(label: string, action: () => Promise<string | void>) {
if (pendingRef.current) return;
pendingRef.current = true;
setPending(label);
setError(null);
setNotice(null);
setFeedback(null);
const section = label === "check" || label === "server" ? "app" : "providers";
const result = await settlePromise(action);
pendingRef.current = false;
setPending(null);
if (AsyncResult.isFailure(result)) {
const cause = squashAtomCommandFailure(result);
setError(
cause instanceof Error ? cause.message : "The action could not be completed. Try again.",
);
setFeedback({
section,
message:
cause instanceof Error ? cause.message : "The action could not be completed. Try again.",
failed: true,
});
} else if (AsyncResult.isSuccess(result) && result.value !== undefined) {
setFeedback({ section, message: result.value, failed: false });
}
}

Expand Down Expand Up @@ -147,7 +156,7 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm
input: { targetVersion },
});
if (AsyncResult.isFailure(result)) throw squashAtomCommandFailure(result);
setNotice(`Updated to ${result.value.targetVersion}.`);
return `Updated to ${result.value.targetVersion}.`;
}),
},
],
Expand Down Expand Up @@ -227,12 +236,6 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm
: "This connection does not have permission to manage the environment."}
</Text>
) : null}
{error ? (
<Text selectable className="px-2 text-sm text-danger-foreground">
{error}
</Text>
) : null}
{notice ? <Text className="px-2 text-sm text-foreground-muted">{notice}</Text> : null}
{config && !outdated ? (
<>
<SettingsSection title={MOBILE_APP_NAME}>
Expand Down Expand Up @@ -273,6 +276,7 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm
: `Update and restart ${MOBILE_APP_NAME} on this machine.`}
</Text>
) : null}
{feedback?.section === "app" ? <FeedbackText {...feedback} /> : null}
</View>
{supportUrl ? (
<SettingsActionRow
Expand Down Expand Up @@ -326,10 +330,15 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm
input: { reloadSecrets: true },
});
if (AsyncResult.isFailure(result)) throw squashAtomCommandFailure(result);
setNotice("Provider status refreshed.");
return "Provider status refreshed.";
});
}}
/>
{feedback?.section === "providers" ? (
<View className="px-4 pb-4">
<FeedbackText {...feedback} />
</View>
) : null}
{config.providers
.filter((provider) => provider.enabled)
.map((provider) => (
Expand Down Expand Up @@ -403,3 +412,14 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm
</SettingsScreen>
);
}

function FeedbackText(props: { readonly message: string; readonly failed: boolean }) {
return (
<Text
selectable={props.failed}
className={props.failed ? "text-sm text-danger-foreground" : "text-sm text-foreground-muted"}
>
{props.message}
</Text>
);
}
2 changes: 1 addition & 1 deletion assets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ Do not edit the generated PNG, ICO, or WebP files directly.

## Mobile launcher, splash, and widget artwork

`vp run icons:export:android` renders the mobile app's assets from `agents/pip.svg` into `apps/mobile/assets/agents/` and the widget's template mark into `apps/mobile/assets/widget/PipMark.svg`. iOS gets a full-bleed navy square because it applies its own mask. Android masks the central 72dp of a 108dp adaptive canvas and the Android 12+ splash screen masks the central two thirds of a 288dp canvas, so Android gets Pip alone, sized to stay inside those safe zones over a navy background. Every app variant uses the same set.
`vp run icons:export:android` renders the mobile app's assets from `agents/pip.svg` into `apps/mobile/assets/agents/` and the widget's template mark into `apps/mobile/assets/widget/PipMark.svg`. iOS gets a full-bleed navy square because it applies its own mask. Android masks the central 72dp of a 108dp adaptive canvas and the Android 12+ splash screen masks the central two thirds of a 288dp canvas, so Android gets Pip alone, sized to stay inside those safe zones over a navy background. Production and preview builds use that set. Development builds use a white Pip over `dev/app-icon.icon`'s blueprint layers, so the home-screen icon matches their in-app mark.
47 changes: 47 additions & 0 deletions scripts/export-android-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ const ANDROID_FRACTION = 0.4;
const SVG_DENSITY = 300;
const OUTPUT_DIRECTORY = "apps/mobile/assets/agents";
const WIDGET_MARK = "apps/mobile/assets/widget/PipMark.svg";
const DEV_ICON_EDGE = "#347FF8";
const DEV_ICON_LAYERS = ["background.svg", "annotations.svg"].map(
(name) => `assets/dev/app-icon.icon/Assets/${name}`,
);

export class AndroidIconRenderError extends Schema.TaggedError<AndroidIconRenderError>()(
"AndroidIconRenderError",
Expand All @@ -53,6 +57,31 @@ const exportAndroidIcons = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const repositoryRoot = path.resolve(import.meta.dirname, "..");
/** The dev blueprint layers, full bleed, with `top` composited over them. */
const layered = (layer: string, size: number, ...top: ReadonlyArray<Buffer>) =>
Effect.forEach(DEV_ICON_LAYERS, (source) =>
fs
.readFileString(path.join(repositoryRoot, source))
.pipe(Effect.flatMap((svg) => rasterize(source, svg, size))),
).pipe(
Effect.flatMap(([background, ...overlays]) =>
Effect.tryPromise({
// sharp composites last, so flatten in a second pass. The blueprint rounds its own
// corners; launchers mask further in, and iOS icons take no alpha.
try: async () =>
sharp(
await sharp(background)
.composite([...overlays, ...top].map((input) => ({ input })))
.png()
.toBuffer(),
)
.flatten({ background: DEV_ICON_EDGE })
.png()
.toBuffer(),
catch: (cause) => new AndroidIconRenderError({ layer, cause }),
}),
),
);
const pip = parsePip(yield* fs.readFileString(path.join(repositoryRoot, PIP_SOURCE)));
const splashMarkHeight = Math.round((SPLASH_MARK_WIDTH * pip.height) / pip.width);

Expand Down Expand Up @@ -103,6 +132,24 @@ const exportAndroidIcons = Effect.gen(function* () {
],
// A template image: iOS tints it, so only the shape matters.
[WIDGET_MARK, pipGlyphSvg(pip, PIP_COLORS.black)],
// Dev builds wear the desktop dev icon's blueprint with a white Pip, matching their in-app mark.
[
`${OUTPUT_DIRECTORY}/dev-ios-icon-1024.png`,
yield* layered(
"dev-ios-icon",
IOS_CANVAS,
yield* canvas("dev-ios-pip", pip, {
size: IOS_CANVAS,
fraction: ICON_FRACTION,
fill: PIP_COLORS.white,
lift: PIP_ICON_LIFT,
}),
),
],
[
`${OUTPUT_DIRECTORY}/android-dev-icon-background.png`,
yield* layered("dev-android-background", ADAPTIVE_CANVAS),
],
];
for (const [relativePath, contents] of outputs) {
yield* fs.writeFile(
Expand Down
Loading