0

Usually, you would need to do something similar to this to cache videos and pictures:

SplashScreen.preventAutoHideAsync();

function cacheAssets(images) {
  return images.map((image) => {
    if (typeof image === "string") {
      return Image.prefetch(image);
    } else {
      return Asset.fromModule(image).downloadAsync();
    }
  });
}

export default function App() {
  const [appIsReady, setAppIsReady] = useState(false);

  // Load any resources or data that you need prior to rendering the app
  useEffect(() => {
    async function loadResourcesAndDataAsync() {
      try {
        const Assets = cacheAssets([
          require("./app/assets/salutt_splash_video_no_tagline.mp4"),
          require("./app/assets/AppCustomBackgroundsFrench/LoginOptionsFR1.png"),
          require("./app/assets/AppCustomBackgroundsFrench/SignInFR1.png"),
          etc.,

        ]);

        await Promise.all([...Assets]);
      } catch (e) {
        // You might want to provide this error information to an error reporting service
        console.warn(e);
      } finally {
        setAppIsReady(true);
        setTimeout(SplashScreen.hideAsync, 400);
      }
    }

    loadResourcesAndDataAsync();
  }, []);

  if (!appIsReady) {
    return null;
  }

Is there a way to just put the name of the directory or use recursive wildcards like **/*.png to chose a bunch of files at the same time?

  • I don't think it's possible, but what I did in a similar situation: I put a script in the project which traverses through folders and subfolders in `assets`, takes note of all existing files in their und generates a JS/TS file with a `require` statement for each single asset file. It's even possible that there're libraries for that. – NotX Dec 25 '22 at 11:41
  • If found some time to provide a longer example. I hope it helps! – NotX Dec 26 '22 at 16:20

1 Answers1

1

I think that's not possible. Instead, I'ld go for a tool which generates the code with all desired require statements.

Here's what I came up with:

In tools/assetBundler.ts:

import { resolve as resolvePath, relative as relativePath } from 'path';
import * as fs from 'fs';

const generatedFolderName = 'generated';
const ignorePattern = /^\.|\w*.(ts|js)$/m;

/**
 * Generates `require` statements for all assets in a new file, so they will get bundled.
 */
const run = async () => {
  try {
    const inputFolder: string = resolvePath('.', 'app', 'assets');
    const targetFolder: string = resolvePath('.', 'app', 'assets', 'assetManager', generatedFolderName);
    const fileNames = await getAllFileNames(inputFolder);

    await ensureEmptyFolder(targetFolder);

    const mapEntries: string[] = [];
    for (const fileName of fileNames)
      mapEntries.push(`['${fileName}', {asset: require('${fileName.replace('app/assets', '../..')}')}],`);
    const bundlerContent = `/**
 * This file was automatically generated by the asset bundler script.
 * DO NOT MODIFY IT BY HAND. Instead, modify the source asset and the data files,
 * and run this script to regenerate this file.
 */
 
 export const assetsByName = new Map<string, {asset: any}>([
${mapEntries.join('\n')}
]);\n`;
    return fs.promises.writeFile(resolvePath(targetFolder, 'assetMap.ts'), bundlerContent);
  } catch (e: unknown) {
    console.log("Couldn't generate asset bundler:", e);
  }
};

/**
 * Based on https://stackoverflow.com/a/45130990/5767484, this method returns a list of all files names in the given folder and subfolders.
 * @param dir the folder where to find the files in
 * @returns the list of all files found
 */
const getAllFileNames: (dir: string) => Promise<string[]> = async (dir) => {
  const dirents = await fs.promises.readdir(dir, { withFileTypes: true });
  const files = await Promise.all(
    dirents.map((dirent) => {
      let fileName: string | null = null;
      if (!ignorePattern.test(dirent.name))
        fileName = relativePath(process.cwd(), resolvePath(dir, dirent.name));
      return dirent.isDirectory() ? getAllFileNames(resolvePath(dir, dirent.name)) : [fileName];
    })
  );
  return files.flat().filter(notEmpty);
};

/**
 * Makes sure the given folder exists and is empty. Note that existing files in the folder we be deleted in case `enforceEmpty` isn't set to true.
 * @param folder to be created and emptied
 * @param enforceEmpty if `true`, existing files in the folder will be deleted
 */
const ensureEmptyFolder = async (folder: string, enforceEmpty: boolean = true) =>
  fs.promises
    .mkdir(folder, { recursive: true })
    .then((_) => (enforceEmpty ? fs.promises.rm(folder, { recursive: true, force: true }) : undefined))
    .then((_) => fs.promises.mkdir(folder))
    .catch((e) => {
      if (enforceEmpty || e.code !== 'EEXIST') throw e;
    });

/** Filter for `null` and `undefined` providing type-safety.*/
function notEmpty<TValue>(value: TValue | null | undefined): value is TValue => value != null

(async () => run())();

To run this tool, you can create npm scripts in your package.json like:

"scripts": {
    "asset-bundler": "ts-node tools/generateAssetBundler.ts",
    "bundle-start": "npm run asset-bundler && expo start "
}

This way you'll get a file like

export const assetsByName = new Map<string, { asset: any }>([
  ['app/assets/salutt_splash_video_no_tagline.mp4', { asset: require('../../salutt_splash_video_no_tagline.mp4') }],
  [ ... ],
  ...
]);

in app/assets/assetManager/assetMap.ts, and you can import your assets by their qualified name, e.g.

const splashVid = assetsByName.get('app/assets/salutt_splash_video_no_tagline.mp4')`

You can create an assetManager which holds the assetsByName map internally if you need convenience functions etc. Note that my code is in TypeScript - I'ld heavily recommend you to use TypeScript, but it can by converted to JavaScript rather easily by deleting some type declarations. Also be careful about the pathes. They might differ a bit for you, so you might have to adjust them accordingly.

NotX
  • 1,516
  • 1
  • 14
  • 28