chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
+28
View File
@@ -0,0 +1,28 @@
import { requireNativeModule } from 'expo-modules-core';
import { UnloadFontOptions } from './Font.types';
export type ExpoFontLoaderModule = {
getLoadedFonts: () => string[];
loadAsync: (fontFamilyName: string, localUriOrWebAsset: any) => Promise<void>;
// the following methods are only available on web
unloadAllAsync?: () => Promise<void>;
unloadAsync?: (fontFamilyName: string, options?: UnloadFontOptions) => Promise<void>;
isLoaded?: (fontFamilyName: string, options?: UnloadFontOptions) => boolean;
getServerResources?: () => string[];
resetServerContext?: () => void;
};
const m: ExpoFontLoaderModule =
typeof window === 'undefined'
? // React server mock
{
getLoadedFonts() {
return [];
},
loadAsync() {
return Promise.resolve();
},
}
: requireNativeModule('ExpoFontLoader');
export default m;
+253
View File
@@ -0,0 +1,253 @@
import { CodedError, registerWebModule } from 'expo-modules-core';
import FontObserver from 'fontfaceobserver';
import type { ExpoFontLoaderModule } from './ExpoFontLoader';
import { UnloadFontOptions } from './Font';
import { FontDisplay, FontResource } from './Font.types';
function getFontFaceStyleSheet(): CSSStyleSheet | null {
if (typeof window === 'undefined') {
return null;
}
const styleSheet = getStyleElement();
return styleSheet.sheet ? (styleSheet.sheet as CSSStyleSheet) : null;
}
type RuleItem = { rule: CSSFontFaceRule; index: number };
function getFontFaceRules(): RuleItem[] {
const sheet = getFontFaceStyleSheet();
if (sheet) {
// @ts-ignore: rule iterator
const rules = [...sheet.cssRules];
const items: RuleItem[] = [];
for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
if (rule instanceof CSSFontFaceRule) {
items.push({ rule, index: i });
}
}
return items;
}
return [];
}
function getFontFaceRulesMatchingResource(
fontFamilyName: string,
options?: UnloadFontOptions
): RuleItem[] {
const rules = getFontFaceRules();
return rules.filter(({ rule }) => {
return (
rule.style.fontFamily === fontFamilyName &&
(options && options.display ? options.display === (rule.style as any).fontDisplay : true)
);
});
}
const serverContext: Set<{ name: string; css: string; resourceId: string }> = new Set();
function getHeadElements(): {
$$type: string;
rel?: string;
href?: string;
as?: string;
crossorigin?: string;
children?: string;
id?: string;
type?: string;
}[] {
const entries = [...serverContext.entries()];
if (!entries.length) {
return [];
}
const css = entries.map(([{ css }]) => css).join('\n');
const links = entries.map(([{ resourceId }]) => resourceId);
// TODO: Maybe return nothing if no fonts were loaded.
return [
{
$$type: 'style',
children: css,
id: ID,
type: 'text/css',
},
...links.map((resourceId) => ({
$$type: 'link',
rel: 'preload',
href: resourceId,
as: 'font',
crossorigin: '',
})),
];
}
const ExpoFontLoader: Required<ExpoFontLoaderModule> = {
async unloadAllAsync(): Promise<void> {
if (typeof window === 'undefined') return;
const element = document.getElementById(ID);
if (element && element instanceof HTMLStyleElement) {
document.removeChild(element);
}
},
async unloadAsync(fontFamilyName: string, options?: UnloadFontOptions): Promise<void> {
const sheet = getFontFaceStyleSheet();
if (!sheet) return;
const items = getFontFaceRulesMatchingResource(fontFamilyName, options);
for (const item of items) {
sheet.deleteRule(item.index);
}
},
getServerResources(): string[] {
const elements = getHeadElements();
return elements
.map((element) => {
switch (element.$$type) {
case 'style':
return `<style id="${element.id}">${element.children}</style>`;
case 'link':
return `<link rel="${element.rel}" href="${element.href}" as="${element.as}" crossorigin="${element.crossorigin}" />`;
default:
return '';
}
})
.filter(Boolean);
},
resetServerContext() {
serverContext.clear();
},
getLoadedFonts(): string[] {
if (typeof window === 'undefined') {
return [...serverContext.values()].map(({ name }) => name);
}
const rules = getFontFaceRules();
return rules.map(({ rule }) => rule.style.fontFamily);
},
isLoaded(fontFamilyName: string, resource: UnloadFontOptions = {}): boolean {
if (typeof window === 'undefined') {
return !![...serverContext.values()].find((asset) => {
return asset.name === fontFamilyName;
});
}
return getFontFaceRulesMatchingResource(fontFamilyName, resource)?.length > 0;
},
// NOTE(vonovak): This is used in RN vector-icons to load fonts dynamically on web. Changing the signature is breaking.
// NOTE(EvanBacon): No async keyword! This cannot return a promise in Node environments.
loadAsync(fontFamilyName: string, resource: FontResource): Promise<void> {
if (__DEV__ && typeof resource !== 'object') {
// to help devving on web, where loadAsync interface is different from native
throw new CodedError(
'ERR_FONT_SOURCE',
`Expected font resource of type \`object\` instead got: ${typeof resource}`
);
}
if (typeof window === 'undefined') {
serverContext.add({
name: fontFamilyName,
css: _createWebFontTemplate(fontFamilyName, resource),
// @ts-expect-error: typeof string
resourceId: resource.uri!,
});
return Promise.resolve();
}
const canInjectStyle = document.head && typeof document.head.appendChild === 'function';
if (!canInjectStyle) {
throw new CodedError(
'ERR_WEB_ENVIRONMENT',
`The browser's \`document.head\` element doesn't support injecting fonts.`
);
}
const style = getStyleElement();
document.head!.appendChild(style);
const res = getFontFaceRulesMatchingResource(fontFamilyName, resource);
if (!res.length) {
_createWebStyle(fontFamilyName, resource);
}
if (!isFontLoadingListenerSupported()) {
return Promise.resolve();
}
return new FontObserver(fontFamilyName, {
// @ts-expect-error: TODO(@kitten): Typings indicate that the polyfill may not support this?
display: resource.display,
}).load(null, 6000);
},
};
const isServer = process.env.EXPO_OS === 'web' && typeof window === 'undefined';
function createExpoFontLoader() {
return ExpoFontLoader;
}
const toExport = isServer
? ExpoFontLoader
: // @ts-expect-error: registerWebModule calls `new` on the module implementation.
// Normally that'd be a class but that doesn't work on server, so we use a function instead.
// TS doesn't like that but we don't need it to be a class.
registerWebModule(createExpoFontLoader, 'ExpoFontLoader');
export default toExport as typeof ExpoFontLoader;
const ID = 'expo-generated-fonts';
function getStyleElement(): HTMLStyleElement {
const element = document.getElementById(ID);
if (element && element instanceof HTMLStyleElement) {
return element;
}
const styleElement = document.createElement('style');
styleElement.id = ID;
return styleElement;
}
export function _createWebFontTemplate(fontFamily: string, resource: FontResource): string {
return `@font-face{font-family:"${fontFamily}";src:url("${resource.uri}");font-display:${
resource.display || FontDisplay.AUTO
}}`;
}
function _createWebStyle(fontFamily: string, resource: FontResource): HTMLStyleElement {
const fontStyle = _createWebFontTemplate(fontFamily, resource);
const styleElement = getStyleElement();
// @ts-ignore: TypeScript does not define HTMLStyleElement::styleSheet. This is just for IE and
// possibly can be removed if it's unnecessary on IE 11.
if (styleElement.styleSheet) {
const styleElementIE = styleElement as any;
styleElementIE.styleSheet.cssText = styleElementIE.styleSheet.cssText
? styleElementIE.styleSheet.cssText + fontStyle
: fontStyle;
} else {
const textNode = document.createTextNode(fontStyle);
styleElement.appendChild(textNode);
}
return styleElement;
}
function isFontLoadingListenerSupported(): boolean {
const { userAgent } = window.navigator;
// WebKit is broken https://github.com/bramstein/fontfaceobserver/issues/95
const isIOS = !!userAgent.match(/iPad|iPhone/i);
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
// Edge is broken https://github.com/bramstein/fontfaceobserver/issues/109#issuecomment-333356795
const isEdge = userAgent.includes('Edge');
// Internet Explorer
const isIE = userAgent.includes('Trident');
// Firefox
const isFirefox = userAgent.includes('Firefox');
return !isSafari && !isIOS && !isEdge && !isIE && !isFirefox;
}
+3
View File
@@ -0,0 +1,3 @@
import { requireOptionalNativeModule } from 'expo-modules-core';
export default requireOptionalNativeModule('ExpoFontUtils');
+11
View File
@@ -0,0 +1,11 @@
import { NativeModule, registerWebModule, UnavailabilityError } from 'expo-modules-core';
import { RenderToImageOptions } from './FontUtils.types';
class ExpoFontUtils extends NativeModule {
async renderToImageAsync(glyphs: string, options?: RenderToImageOptions): Promise<string> {
throw new UnavailabilityError('expo-font', 'renderToImageAsync');
}
}
export default registerWebModule(ExpoFontUtils, 'ExpoFontUtils');
+226
View File
@@ -0,0 +1,226 @@
import { CodedError, Platform, UnavailabilityError } from 'expo-modules-core';
import ExpoFontLoader from './ExpoFontLoader';
import { FontDisplay, FontSource, FontResource, UnloadFontOptions } from './Font.types';
import { getAssetForSource, loadSingleFontAsync } from './FontLoader';
import {
isLoadedInCache,
isLoadedNative,
loadPromises,
markLoaded,
purgeCache,
purgeFontFamilyFromCache,
} from './memory';
import { registerStaticFont } from './server';
// @needsAudit
/**
* Synchronously detect if the font for `fontFamily` has finished loading.
*
* @param fontFamily The name used to load the `FontResource`.
* @return Returns `true` if the font has fully loaded.
*/
export function isLoaded(fontFamily: string): boolean {
if (Platform.OS === 'web') {
if (typeof ExpoFontLoader.isLoaded !== 'function') {
throw new Error(
`expected ExpoFontLoader.isLoaded to be a function, was ${typeof ExpoFontLoader.isLoaded}`
);
}
return isLoadedInCache(fontFamily) || ExpoFontLoader.isLoaded(fontFamily);
}
return isLoadedNative(fontFamily);
}
/**
* Synchronously get all the fonts that have been loaded.
* This includes fonts that were bundled at build time using the config plugin, as well as those loaded at runtime using `loadAsync`.
*
* @returns Returns array of strings which you can use as `fontFamily` [style prop](https://reactnative.dev/docs/text#style).
*/
export function getLoadedFonts(): string[] {
return ExpoFontLoader.getLoadedFonts();
}
// @needsAudit
/**
* Synchronously detect if the font for `fontFamily` is still being loaded.
*
* @param fontFamily The name used to load the `FontResource`.
* @returns Returns `true` if the font is still loading.
*/
export function isLoading(fontFamily: string): boolean {
return fontFamily in loadPromises;
}
// @needsAudit
/**
* An efficient method for loading fonts from static or remote resources which can then be used
* with the platform's native text elements. In the browser, this generates a `@font-face` block in
* a shared style sheet for fonts. No CSS is needed to use this method.
*
* > **Note**: We recommend using the [config plugin](#configuration-in-app-config) instead whenever possible.
*
* @param fontFamilyOrFontMap String or map of values that can be used as the `fontFamily` [style prop](https://reactnative.dev/docs/text#style)
* with React Native `Text` elements.
* @param source The font asset that should be loaded into the `fontFamily` namespace.
*
* @return Returns a promise that fulfils when the font has loaded. Often you may want to wrap the
* method in a `try/catch/finally` to ensure the app continues if the font fails to load.
*/
export function loadAsync(
fontFamilyOrFontMap: string | Record<string, FontSource>,
source?: FontSource
): Promise<void> {
// NOTE(EvanBacon): Static render pass on web must be synchronous to collect all fonts.
// Because of this, `loadAsync` doesn't use the `async` keyword and deviates from the
// standard Expo SDK style guide.
const isServer = Platform.OS === 'web' && typeof window === 'undefined';
if (typeof fontFamilyOrFontMap === 'object') {
if (source) {
return Promise.reject(
new CodedError(
`ERR_FONT_API`,
`No fontFamily can be used for the provided source: ${source}. The second argument of \`loadAsync()\` can only be used with a \`string\` value as the first argument.`
)
);
}
const fontMap = fontFamilyOrFontMap;
const names = Object.keys(fontMap);
if (isServer) {
names.map((name) => registerStaticFont(name, fontMap[name]));
return Promise.resolve();
}
return Promise.all(names.map((name) => loadFontInNamespaceAsync(name, fontMap[name]))).then(
() => {}
);
}
if (isServer) {
registerStaticFont(fontFamilyOrFontMap, source);
return Promise.resolve();
}
return loadFontInNamespaceAsync(fontFamilyOrFontMap, source);
}
async function loadFontInNamespaceAsync(
fontFamily: string,
source?: FontSource | null
): Promise<void> {
if (!source) {
throw new CodedError(
`ERR_FONT_SOURCE`,
`Cannot load null or undefined font source: { "${fontFamily}": ${source} }. Expected asset of type \`FontSource\` for fontFamily of name: "${fontFamily}"`
);
}
// we consult the native module to see if the font is already loaded
// this is slower than checking the cache but can help avoid loading the same font n times
if (isLoaded(fontFamily)) {
return;
}
if (loadPromises.hasOwnProperty(fontFamily)) {
return loadPromises[fontFamily];
}
// Important: we want all callers that concurrently try to load the same font to await the same
// promise. If we're here, we haven't created the promise yet. To ensure we create only one
// promise in the program, we need to create the promise synchronously without yielding the event
// loop from this point.
const asset = getAssetForSource(source);
loadPromises[fontFamily] = (async () => {
try {
await loadSingleFontAsync(fontFamily, asset);
markLoaded(fontFamily);
} finally {
delete loadPromises[fontFamily];
}
})();
await loadPromises[fontFamily];
}
// @needsAudit
/**
* Unloads all the custom fonts. This is used for testing.
* @hidden
*/
export async function unloadAllAsync(): Promise<void> {
if (!ExpoFontLoader.unloadAllAsync) {
throw new UnavailabilityError('expo-font', 'unloadAllAsync');
}
if (Object.keys(loadPromises).length) {
throw new CodedError(
`ERR_UNLOAD`,
`Cannot unload fonts while they're still loading: ${Object.keys(loadPromises).join(', ')}`
);
}
purgeCache();
await ExpoFontLoader.unloadAllAsync();
}
// @needsAudit
/**
* Unload custom fonts matching the `fontFamily`s and display values provided.
* This is used for testing.
*
* @param fontFamilyOrFontMap The name or names of the custom fonts that will be unloaded.
* @param options When `fontFamilyOrFontMap` is a string, this should be the font source used to load
* the custom font originally.
* @hidden
*/
export async function unloadAsync(
fontFamilyOrFontMap: string | Record<string, UnloadFontOptions>,
options?: UnloadFontOptions
): Promise<void> {
if (!ExpoFontLoader.unloadAsync) {
throw new UnavailabilityError('expo-font', 'unloadAsync');
}
if (typeof fontFamilyOrFontMap === 'object') {
if (options) {
throw new CodedError(
`ERR_FONT_API`,
`No fontFamily can be used for the provided options: ${options}. The second argument of \`unloadAsync()\` can only be used with a \`string\` value as the first argument.`
);
}
const fontMap = fontFamilyOrFontMap;
const names = Object.keys(fontMap);
await Promise.all(names.map((name) => unloadFontInNamespaceAsync(name, fontMap[name])));
return;
}
return await unloadFontInNamespaceAsync(fontFamilyOrFontMap, options);
}
async function unloadFontInNamespaceAsync(
fontFamily: string,
options?: UnloadFontOptions
): Promise<void> {
if (!isLoaded(fontFamily)) {
return;
} else {
purgeFontFamilyFromCache(fontFamily);
}
// Important: we want all callers that concurrently try to load the same font to await the same
// promise. If we're here, we haven't created the promise yet. To ensure we create only one
// promise in the program, we need to create the promise synchronously without yielding the event
// loop from this point.
if (!fontFamily) {
throw new CodedError(`ERR_FONT_FAMILY`, `Cannot unload an empty name`);
}
if (!ExpoFontLoader.unloadAsync) {
throw new UnavailabilityError('expo-font', 'unloadAsync');
}
await ExpoFontLoader.unloadAsync(fontFamily, options);
}
export { FontDisplay, FontSource, FontResource, UnloadFontOptions };
+73
View File
@@ -0,0 +1,73 @@
import { Asset } from 'expo-asset';
// @needsAudit
/**
* The different types of assets you can provide to the [`loadAsync()`](#loadasyncfontfamilyorfontmap-source) function.
* A font source can be a URI, a module ID, or an Expo Asset.
*/
export type FontSource = string | number | Asset | FontResource;
// @needsAudit
/**
* An object used to dictate the resource that is loaded into the provided font namespace when used
* with [`loadAsync`](#loadasyncfontfamilyorfontmap-source).
*/
export type FontResource = {
uri?: string | number;
/**
* Sets the [`font-display`](#fontdisplay) property for a given typeface in the browser.
* @platform web
*/
display?: FontDisplay;
default?: string;
};
// @needsAudit
/**
* Sets the [font-display](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display)
* for a given typeface. The default font value on web is `FontDisplay.AUTO`.
* Even though setting the `fontDisplay` does nothing on native platforms, the default behavior
* emulates `FontDisplay.SWAP` on flagship devices like iOS, Samsung, Pixel, etc. Default
* functionality varies on One Plus devices. In the browser this value is set in the generated
* `@font-face` CSS block and not as a style property meaning you cannot dynamically change this
* value based on the element it's used in.
* @platform web
*/
export enum FontDisplay {
/**
* __(Default)__ The font display strategy is defined by the user agent or platform.
* This generally defaults to the text being invisible until the font is loaded.
* Good for buttons or banners that require a specific treatment.
*/
AUTO = 'auto',
/**
* Fallback text is rendered immediately with a default font while the desired font is loaded.
* This is good for making the content appear to load instantly and is usually preferred.
*/
SWAP = 'swap',
/**
* The text will be invisible until the font has loaded. If the font fails to load then nothing
* will appear - it's best to turn this off when debugging missing text.
*/
BLOCK = 'block',
/**
* Splits the behavior between `SWAP` and `BLOCK`.
* There will be a [100ms timeout](https://developers.google.com/web/updates/2016/02/font-display?hl=en)
* where the text with a custom font is invisible, after that the text will either swap to the
* styled text or it'll show the unstyled text and continue to load the custom font. This is good
* for buttons that need a custom font but should also be quickly available to screen-readers.
*/
FALLBACK = 'fallback',
/**
* This works almost identically to `FALLBACK`, the only difference is that the browser will
* decide to load the font based on slow connection speed or critical resource demand.
*/
OPTIONAL = 'optional',
}
// @needsAudit
/**
* Object used to query fonts for unloading.
* @hidden
*/
export type UnloadFontOptions = Pick<FontResource, 'display'>;
+73
View File
@@ -0,0 +1,73 @@
import { useEffect, useState } from 'react';
import { loadAsync, isLoaded } from './Font';
import { FontSource } from './Font.types';
function isMapLoaded(map: string | Record<string, FontSource>) {
if (typeof map === 'string') {
return isLoaded(map);
} else {
return Object.keys(map).every((fontFamily) => isLoaded(fontFamily));
}
}
function useRuntimeFonts(map: string | Record<string, FontSource>): [boolean, Error | null] {
const [loaded, setLoaded] = useState(
// For web rehydration, we need to check if the fonts are already loaded during the static render.
// Native will also benefit from this optimization.
isMapLoaded(map)
);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let isMounted = true;
loadAsync(map)
.then(() => {
if (isMounted) {
setLoaded(true);
}
})
.catch((error) => {
if (isMounted) {
setError(error);
}
});
return () => {
isMounted = false;
};
}, []);
return [loaded, error];
}
function useStaticFonts(map: string | Record<string, FontSource>): [boolean, Error | null] {
loadAsync(map);
return [true, null];
}
// @needsAudit
/**
* Load a map of fonts at runtime with [`loadAsync`](#loadasyncfontfamilyorfontmap-source). This returns a `boolean` if the fonts are
* loaded and ready to use. It also returns an error if something went wrong, to use in development.
*
* > Note, the fonts are not "reloaded" when you dynamically change the font map.
*
* @param map A map of `fontFamily`s to [`FontSource`](#fontsource)s. After loading the font you can
* use the key in the `fontFamily` style prop of a `Text` element.
*
* @return
* - __loaded__ (`boolean`) - A boolean to detect if the font for `fontFamily` has finished
* loading.
* - __error__ (`Error | null`) - An error encountered when loading the fonts.
*
* @example
* ```tsx
* const [loaded, error] = useFonts({
* 'Inter-Black': require('./assets/fonts/Inter-Black.otf'),
* });
* ```
*/
export const useFonts: (map: string | Record<string, FontSource>) => [boolean, Error | null] =
typeof window === 'undefined' ? useStaticFonts : useRuntimeFonts;
+40
View File
@@ -0,0 +1,40 @@
import { Asset } from 'expo-asset';
import { CodedError } from 'expo-modules-core';
import ExpoFontLoader from './ExpoFontLoader';
import { FontResource, FontSource } from './Font.types';
export function getAssetForSource(source: FontSource): Asset | FontResource {
if (source instanceof Asset) {
return source;
}
if (typeof source === 'string') {
return Asset.fromURI(source);
} else if (typeof source === 'number') {
return Asset.fromModule(source);
} else if (typeof source === 'object' && typeof source.uri !== 'undefined') {
return getAssetForSource(source.uri);
}
return source;
}
export async function loadSingleFontAsync(
name: string,
input: Asset | FontResource
): Promise<void> {
const asset = input as Asset;
if (!asset.downloadAsync) {
throw new CodedError(
`ERR_FONT_SOURCE`,
'`loadSingleFontAsync` expected resource of type `Asset` from expo-asset on native'
);
}
await asset.downloadAsync();
if (!asset.downloaded) {
throw new CodedError(`ERR_DOWNLOAD`, `Failed to download asset for font "${name}"`);
}
await ExpoFontLoader.loadAsync(name, asset.localUri);
}
+65
View File
@@ -0,0 +1,65 @@
import { Asset } from 'expo-asset';
import { CodedError } from 'expo-modules-core';
import ExpoFontLoader from './ExpoFontLoader';
import { FontResource, FontSource, FontDisplay } from './Font.types';
function uriFromFontSource(asset: FontSource): string | number | null {
if (typeof asset === 'string') {
return asset || null;
} else if (typeof asset === 'number') {
return uriFromFontSource(Asset.fromModule(asset));
} else if (typeof asset === 'object' && typeof asset.uri === 'number') {
return uriFromFontSource(asset.uri);
} else if (typeof asset === 'object') {
return asset.uri || (asset as Asset).localUri || (asset as FontResource).default || null;
}
return null;
}
function displayFromFontSource(asset: FontSource): FontDisplay {
if (typeof asset === 'object' && 'display' in asset) {
return asset.display || FontDisplay.AUTO;
}
return FontDisplay.AUTO;
}
export function getAssetForSource(source: FontSource): Asset | FontResource {
const uri = uriFromFontSource(source);
const display = displayFromFontSource(source);
if (!uri || typeof uri !== 'string') {
throwInvalidSourceError(uri);
}
return {
uri,
display,
};
}
function throwInvalidSourceError(source: any): never {
let type: string = typeof source;
if (type === 'object') type = JSON.stringify(source, null, 2);
throw new CodedError(
`ERR_FONT_SOURCE`,
`Expected font asset of type \`string | FontResource | Asset\` instead got: ${type}`
);
}
// NOTE(EvanBacon): No async keyword!
export function loadSingleFontAsync(name: string, input: Asset | FontResource): Promise<void> {
if (typeof input !== 'object' || typeof input.uri !== 'string' || (input as any).downloadAsync) {
throwInvalidSourceError(input);
}
try {
return ExpoFontLoader.loadAsync(name, input);
} catch {
// No-op.
}
return Promise.resolve();
}
+29
View File
@@ -0,0 +1,29 @@
import { UnavailabilityError } from 'expo-modules-core';
import { processColor } from 'react-native';
import ExpoFontUtils from './ExpoFontUtils';
import type { RenderToImageOptions, RenderToImageResult } from './FontUtils.types';
export type { RenderToImageOptions, RenderToImageResult };
/**
* Creates an image with provided text.
* @param glyphs Text to be exported.
* @param options RenderToImageOptions.
* @return Promise which fulfils with image metadata.
* @platform android
* @platform ios
*/
export async function renderToImageAsync(
glyphs: string,
options?: RenderToImageOptions
): Promise<RenderToImageResult> {
if (!ExpoFontUtils) {
throw new UnavailabilityError('expo-font', 'ExpoFontUtils.renderToImageAsync');
}
return await ExpoFontUtils.renderToImageAsync(glyphs, {
...options,
color: options?.color ? processColor(options.color) : undefined,
});
}
+39
View File
@@ -0,0 +1,39 @@
export interface RenderToImageOptions {
/**
* Font family name.
* @default system default
*/
fontFamily?: string;
/**
* Size of the font.
* @default 24
*/
size?: number;
/**
* Font color
* @default 'black'
*/
color?: string;
}
// RenderToImageResult needs to be usable as the `source` prop for image,
// so it must stay compatible with ImageURISource type
export interface RenderToImageResult {
/**
* The file uri to the image.
*/
uri: string;
/**
* Image width in dp.
*/
width: number;
/**
* Image height in dp.
*/
height: number;
/**
* Scale factor of the image. Multiply the dp dimensions by this value to get the dimensions in pixels.
* */
scale: number;
}
+5
View File
@@ -0,0 +1,5 @@
export * from './Font';
export function useFonts() {
return [];
}
+3
View File
@@ -0,0 +1,3 @@
export * from './Font';
export * from './FontUtils';
export { useFonts } from './FontHooks';
+42
View File
@@ -0,0 +1,42 @@
import ExpoFontLoader from './ExpoFontLoader';
export const loadPromises: { [name: string]: Promise<void> } = {};
// cache the value on the js side for fast access to the fonts that are loaded
let cache: { [name: string]: boolean } = {};
export function markLoaded(fontFamily: string) {
cache[fontFamily] = true;
}
export function isLoadedInCache(fontFamily: string): boolean {
return fontFamily in cache;
}
export function isLoadedNative(fontFamily: string): boolean {
if (isLoadedInCache(fontFamily)) {
return true;
} else {
const loadedNativeFonts: string[] = ExpoFontLoader.getLoadedFonts();
// NOTE(brentvatne): Bail out here if there are no loaded fonts. This
// is functionally equivalent to the behavior below if the returned array
// is empty, but this handles improper mocking of `getLoadedFonts`.
if (!loadedNativeFonts?.length) {
return false;
}
loadedNativeFonts.forEach((font) => {
cache[font] = true;
});
return fontFamily in cache;
}
}
export function purgeFontFamilyFromCache(fontFamily: string): void {
delete cache[fontFamily];
}
export function purgeCache(): void {
cache = {};
}
+40
View File
@@ -0,0 +1,40 @@
import { CodedError, UnavailabilityError } from 'expo-modules-core';
import ExpoFontLoader from './ExpoFontLoader';
import { FontSource } from './Font.types';
import { getAssetForSource, loadSingleFontAsync } from './FontLoader';
/**
* @returns the server resources that should be statically extracted.
* @private
*/
export function getServerResources(): string[] {
if (!ExpoFontLoader.getServerResources) {
throw new UnavailabilityError('expo-font', 'getServerResources');
}
return ExpoFontLoader.getServerResources();
}
/**
* @returns clear the server resources from the global scope.
* @private
*/
export function resetServerContext() {
if (!ExpoFontLoader.resetServerContext) {
throw new UnavailabilityError('expo-font', 'resetServerContext');
}
return ExpoFontLoader.resetServerContext();
}
export function registerStaticFont(fontFamily: string, source?: FontSource | null) {
// MUST BE A SYNC FUNCTION!
if (!source) {
throw new CodedError(
`ERR_FONT_SOURCE`,
`Cannot load null or undefined font source: { "${fontFamily}": ${source} }. Expected asset of type \`FontSource\` for fontFamily of name: "${fontFamily}"`
);
}
const asset = getAssetForSource(source);
loadSingleFontAsync(fontFamily, asset);
}
@@ -0,0 +1 @@
/// <reference path="../../../expo-asset/src/ts-declarations/react-native-assets.d.ts" />