export const theme = {
  bg: "#312652",
  fg: "#f0f0ff",
  primary: "#fabe32",
};

export function meta({ file }: { file: MediaFile }) {
  if (file.path === "/") return { title: "clo's files" };
  return { title: file.basename + " - clo's files" };
}

// TODO: use rules.ts
export const extensionToViewer: {
  [key: string]: (props: {
    file: MediaFile;
    siblingFiles?: MediaFile[];
  }) => any;
} = {
  ".html": IframeView,
  ".txt": TextView,
  ".md": TextView,
  ".mp4": VideoView,
  ".mkv": VideoView,
  ".webm": VideoView,
  ".avi": VideoView,
  ".mov": VideoView,
  ".mp3": AudioView,
  ".flac": AudioView,
  ".wav": AudioView,
  ".ogg": AudioView,
  ".m4a": AudioView,
  ".jpg": ImageView,
  ".jpeg": ImageView,
  ".png": ImageView,
  ".gif": ImageView,
  ".webp": ImageView,
  ".avif": ImageView,
  ".heic": ImageView,
  ".svg": ImageView,
  ".chat": ChatView,
  ".json": CodeView,
  ".jsonc": CodeView,
  ".toml": CodeView,
  ".ts": CodeView,
  ".js": CodeView,
  ".tsx": CodeView,
  ".jsx": CodeView,
  ".css": CodeView,
  ".py": CodeView,
  ".lua": CodeView,
  ".sh": CodeView,
  ".bat": CodeView,
  ".ps1": CodeView,
  ".cmd": CodeView,
  ".yaml": CodeView,
  ".yml": CodeView,
  ".xml": CodeView,
  ".zig": CodeView,
  ".astro": CodeView,
  ".mdx": CodeView,
  ".php": CodeView,
  ".diff": CodeView,
  ".patch": CodeView,
};

export default function MediaList({
  file,
  hasCotyledonCookie,
}: {
  file: MediaFile;
  hasCotyledonCookie: boolean;
}) {
  addScript("./clofi.client.ts");

  const dirs: MediaFile[] = [];
  let dir: MediaFile | null = file;
  assert(dir);
  do {
    dirs.unshift(dir);
    dir = dir?.getParent();
  } while (dir);
  const parts = file.path.split("/");

  const isCotyledon = parseInt(parts[1]) < 2025;

  return (
    <div class={"files" + (isCotyledon ? " ctld ctld-" + parts[1] : "")}>
      {dirs.map((entry, i) => {
        const isLast = i === dirs.length - 1;
        return (
          <MediaPanel
            file={entry}
            isLast={isLast}
            activeFilename={parts[i + 1]}
            hasCotyledonCookie={hasCotyledonCookie}
          />
        );
      })}
    </div>
  );
}

const specialCaseViewers: Record<string, any> = {
  "/2024/for everyone": ForEveryone,
};

export function MediaPanel({
  file,
  isLast,
  activeFilename,
  hasCotyledonCookie,
}: {
  file: MediaFile;
  isLast: boolean;
  activeFilename: string | null;
  hasCotyledonCookie: boolean;
}) {
  const showsActions = file.path !== "/2024/for everyone";
  const label = file.path === "/"
    ? (
      "clo's files"
    )
    : (
      <>
        {file.kind === MediaFileKind.directory
          ? (
            <span>
              {file.basename}
              <span class="bold-slash">/</span>
            </span>
          )
          : (
            <>
              {file.basename}
              <div style="flex:1" />
              {showsActions && (
                <div class="actions">
                  <a
                    class="custom full-screen"
                    href={`/file${escapeUri(file.path)}?view=embed`}
                    aria-label="full screen"
                  >
                  </a>
                  <a
                    class="custom download"
                    href={`/file${escapeUri(file.path)}?dl`}
                    aria-label="download"
                  >
                  </a>
                </div>
              )}
            </>
          )}
      </>
    );
  let View = specialCaseViewers[file.path] ??
    (extensionToViewer[file.extension.toLowerCase()] as any) ??
    DownloadView;
  if (View === VideoView && file.size > 500_000_000) {
    View = DownloadViewTooBig;
  }
  const mobileBtn = file.path !== "/" && (
    <a
      href={`/file${escapeUri(path.dirname(file.path).replace(/\/$/, ""))}`}
      className="custom mobile-back"
    />
  );
  const canvases: Record<string, string> = {
    "/2017": "2017",
    "/2018": "2018",
    "/2019": "2019",
    "/2020": "2020",
    "/2021": "2021",
    "/2022": "2022",
    "/2023": "2023",
    "/2024": "2024",
  };
  if (canvases[file.path]) {
    addScript(`file-viewer/canvas_${canvases[file.path]}.client.ts`);
  }
  return (
    <div
      class={(isLast ? "panel last" : "panel") +
        (file.path === "/" ? " first" : "")}
    >
      {canvases[file.path] && (
        <canvas
          class="fullscreen-canvas"
          data-canvas={canvases[file.path]}
        >
        </canvas>
      )}
      {file.kind === MediaFileKind.directory && (
        <a class="custom header" href={`/file${escapeUri(file.path)}`}>
          <div class="ico ico-dir-open"></div>
          {label}
        </a>
      )}
      <div class="header">
        {mobileBtn}
        {<div class={`ico ico-${fileIcon(file, true)}`}></div>}
        {label}
      </div>

      {file.kind === MediaFileKind.directory
        ? (
          <DirView
            dir={file}
            activeFilename={activeFilename}
            isLast={isLast}
            hasCotyledonCookie={hasCotyledonCookie}
          />
        )
        : (
          <div
            class={`content file-view${
              View.class ? " file-view-" + View.class : ""
            }`}
          >
            <View file={file} />
          </div>
        )}
    </div>
  );
}

const readmeFile = "readme.txt";
const priorityFiles = ["readme.txt", "index.html"];

function sorterFromDirSort(dirsort: string) {
  const array: string[] = JSON.parse(dirsort);
  if (array.length === 1 && array[0] === "a-z") {
    return sortAlphabetically;
  }
  return (a: MediaFile, b: MediaFile) => {
    const aIndex = array.indexOf(a.basename);
    const bIndex = array.indexOf(b.basename);
    if (bIndex == -1 && aIndex >= 0) return -1;
    if (aIndex == -1 && bIndex >= 0) return 1;
    if (aIndex >= 0 && bIndex >= 0) {
      return aIndex - bIndex;
    }
    return sortDefault(a, b);
  };
}

function sortNumerically(a: MediaFile, b: MediaFile) {
  const n1 = parseInt(a.basenameWithoutExt);
  const n2 = parseInt(a.basenameWithoutExt);
  return n1 - n2;
}

function sortDefault(a: MediaFile, b: MediaFile) {
  // First check if either file is in the priority list
  const aIndex = priorityFiles.indexOf(a.basename.toLowerCase());
  const bIndex = priorityFiles.indexOf(b.basename.toLowerCase());

  if (aIndex !== -1 && bIndex !== -1) {
    // Both are priority files, sort by priority order
    return aIndex - bIndex;
  } else if (aIndex !== -1) {
    // Only a is a priority file
    return -1;
  } else if (bIndex !== -1) {
    // Only b is a priority file
    return 1;
  }

  // Then sort directories before files
  if (a.kind !== b.kind) {
    return a.kind === MediaFileKind.directory ? -1 : 1;
  }

  // Finally sort by date (newest first), then by name (a-z) if dates are the same
  const dateComparison = b.date.getTime() - a.date.getTime();
  if (dateComparison !== 0) {
    return dateComparison;
  }
  // If dates are the same, sort alphabetically by name
  return a.basename.localeCompare(b.basename);
}

function sortAlphabetically(a: MediaFile, b: MediaFile) {
  // First check if either file is in the priority list
  const aIndex = priorityFiles.indexOf(a.basename.toLowerCase());
  const bIndex = priorityFiles.indexOf(b.basename.toLowerCase());

  if (aIndex !== -1 && bIndex !== -1) {
    // Both are priority files, sort by priority order
    return aIndex - bIndex;
  } else if (aIndex !== -1) {
    // Only a is a priority file
    return -1;
  } else if (bIndex !== -1) {
    // Only b is a priority file
    return 1;
  }

  // If dates are the same, sort alphabetically by name
  return a.basename.localeCompare(b.basename);
}

function isNumericallyOrdered(files: MediaFile[]) {
  return !files.some((x) => Number.isNaN(parseInt(x.basenameWithoutExt)));
}

function sortChronologicalStartToEnd(a: MediaFile, b: MediaFile) {
  const aIndex = priorityFiles.indexOf(a.basename.toLowerCase());
  const bIndex = priorityFiles.indexOf(b.basename.toLowerCase());

  if (aIndex !== -1 && bIndex !== -1) {
    return aIndex - bIndex;
  } else if (aIndex !== -1) {
    return -1;
  } else if (bIndex !== -1) {
    return 1;
  }

  const dateComparison = a.date.getTime() - b.date.getTime();
  if (dateComparison !== 0) {
    return dateComparison;
  }
  return a.basename.localeCompare(b.basename);
}

const sortOverrides: Record<string, (a: MediaFile, b: MediaFile) => number> = {
  "/2024": sortChronologicalStartToEnd,
  "/2023": sortChronologicalStartToEnd,
  "/2022": sortChronologicalStartToEnd,
  "/2021": sortChronologicalStartToEnd,
  "/2020": sortChronologicalStartToEnd,
  "/2019": sortChronologicalStartToEnd,
  "/2018": sortChronologicalStartToEnd,
  "/2017": sortChronologicalStartToEnd,
};

function DirView({
  dir,
  activeFilename,
  isLast,
  hasCotyledonCookie,
}: {
  dir: MediaFile;
  activeFilename: string | null;
  isLast: boolean;
  hasCotyledonCookie: boolean;
}) {
  if (dir.path === "/") {
    return (
      <RootDirView
        dir={dir}
        activeFilename={activeFilename}
        isLast={isLast}
        hasCotyledonCookie={hasCotyledonCookie}
      />
    );
  }
  const isCotyledon = parseInt(dir.path.split("/")?.[1]) < 2025;
  const unsortedFiles = dir
    .getPublicChildren()
    .filter((f) => !f.basenameWithoutExt.startsWith("_unlisted"));
  const sorter = dir.dirsort
    ? sorterFromDirSort(dir.dirsort)
    : isNumericallyOrdered(unsortedFiles)
    ? sortNumerically
    : isCotyledon
    ? sortChronologicalStartToEnd
    : (sortOverrides[dir.path] ?? sortDefault);
  const sortedFiles = unsortedFiles.sort(sorter);
  const readme = sortedFiles.find((f) => f.basename === readmeFile);
  if (readme && isLast) activeFilename ||= readmeFile;
  const main = (
    <div class="content primary">
      <ul>
        {sortedFiles.map((file) => {
          return (
            <ListItem file={file} active={activeFilename === file.basename} />
          );
        })}
      </ul>
    </div>
  );

  if (readme && isLast) {
    return (
      <div class="hsplit">
        <ReadmeView file={readme} siblingFiles={sortedFiles} />
        {main}
      </div>
    );
  }

  return main;
}

const unknownDate = new Date("1970-01-03");
const unknownDateWithKnownYear = new Date("1970-02-20");

function ListItem({
  file,
  active,
  noDate,
}: {
  file: MediaFile;
  active: boolean;
  noDate?: boolean;
}) {
  const dateTime = file.date;
  let shortDate = dateTime < unknownDateWithKnownYear
    ? (
      dateTime < unknownDate
        ? (
          "??.??.??"
        )
        : <>xx.xx.{21 + Math.floor(dateTime.getTime() / 86400000)}</>
    )
    : (
      `${(dateTime.getMonth() + 1).toString().padStart(2, "0")}.${
        dateTime
          .getDate()
          .toString()
          .padStart(2, "0")
      }.${dateTime.getFullYear().toString().slice(2)}`
    );

  let basenameWithoutExt = file.basenameWithoutExt;

  let meta = file.kind === MediaFileKind.directory
    ? formatSize(file.size)
    : (file.duration ?? 0) > 0
    ? formatDuration(file.duration!)
    : null;
  if (meta && dirname(file.path) !== "/") {
    meta = `(${meta})`;
  }

  const isReadme = file.basename === readmeFile;

  return (
    <li>
      <a
        class={(active ? "active " : "") + (isReadme ? "readme " : "") +
          "li custom"}
        href={file.extension === ".lnk"
          ? file.contents
          : isReadme
          ? `/file${escapeUri(path.dirname(file.path).replace(/\/$/, ""))}`
          : `/file${escapeUri(file.path)}`}
      >
        <span>
          {file.basename === readmeFile
            ? <>{!noDate && <div class="line"></div>}</>
            : <>{!noDate && <date>{shortDate}</date>}</>}
          <span class="size mobile">{meta}</span>
        </span>

        <span>
          {path.dirname(file.path) !== "/" && (
            <span className={`ico ico-${fileIcon(file)}`}></span>
          )}
          {basenameWithoutExt}
          <span class="ext">{file.extension}</span>
          {file.kind === MediaFileKind.directory
            ? <span class="bold-slash">/</span>
            : (
              ""
            )}

          <span class="size desktop">{meta}</span>
        </span>
      </a>
    </li>
  );
}

function fileIcon(file: MediaFile, dirOpen?: boolean) {
  if (file.kind === MediaFileKind.directory) {
    return dirOpen ? "dir-open" : "dir";
  }
  if (file.basename === "readme.txt") {
    return "readme";
  }
  if (file.path === "/2024/for everyone") {
    return "snow";
  }
  const ext = path.extname(file.basename).toLowerCase();
  if (
    ext === ".mp4" ||
    ext === ".mkv" ||
    ext === ".webm" ||
    ext === ".avi" ||
    ext === ".mov"
  ) {
    return "video";
  }
  if (
    ext === ".mp3" ||
    ext === ".flac" ||
    ext === ".wav" ||
    ext === ".ogg" ||
    ext === ".m4a"
  ) {
    return "audio";
  }
  if (
    ext === ".jpg" ||
    ext === ".jpeg" ||
    ext === ".png" ||
    ext === ".gif" ||
    ext === ".webp" ||
    ext === ".avif" ||
    ext === ".heic" ||
    ext === ".svg"
  ) {
    return "image";
  }
  if (ext === ".comp" || ext === ".fuse" || ext === ".setting") return "fusion";
  if (ext === ".txt" || ext === ".md") return "text";
  if (ext === ".html") return "webpage";
  if (ext === ".blend") return "blend";
  if (
    ext === ".zip" ||
    ext === ".rar" ||
    ext === ".7z" ||
    ext === ".tar" ||
    ext === ".gz" ||
    ext === ".bz2" ||
    ext === ".xz"
  ) {
    return "archive";
  }
  if (ext === ".lnk") return "link";
  if (ext === ".chat") return "chat";
  if (
    ext === ".ts" ||
    ext === ".js" ||
    ext === ".tsx" ||
    ext === ".jsx" ||
    ext === ".css" ||
    ext === ".py" ||
    ext === ".lua" ||
    ext === ".sh" ||
    ext === ".bat" ||
    ext === ".ps1" ||
    ext === ".cmd" ||
    ext === ".php"
  ) {
    return "code";
  }
  if (ext === ".json" || ext === ".toml" || ext === ".yaml" || ext === ".yml") {
    return "json";
  }
  return "file";
}

function RootDirView({
  dir,
  activeFilename,
  isLast,
  hasCotyledonCookie,
}: {
  dir: MediaFile;
  activeFilename: string | null;
  isLast: boolean;
  hasCotyledonCookie: boolean;
}) {
  const children = dir.getPublicChildren();
  let readme: MediaFile | null = null;

  const groups = {
    // years 2025 and onwards
    years: [] as MediaFile[],
    // named categories
    categories: [] as MediaFile[],
    // years 2017 to 2024
    cotyledon: [] as MediaFile[],
  };
  const colorMap = {
    years: "#a2ff91",
    categories: "#9c91ff",
    cotyledon: "#ff91ca",
  };
  for (const child of children) {
    const basename = child.basename;
    if (basename === readmeFile) {
      readme = child;
      continue;
    }

    const year = basename.match(/^(\d{4})/);
    if (year) {
      const n = parseInt(year[1]);
      if (n >= 2025) {
        groups.years.push(child);
      } else {
        groups.cotyledon.push(child);
      }
    } else {
      groups.categories.push(child);
    }
  }

  if (readme && isLast) activeFilename ||= readmeFile;

  const main = (
    <div class="content primary">
      {readme && (
        <ul>
          <ListItem
            file={readme}
            active={activeFilename === readmeFile}
            noDate
          />
        </ul>
      )}
      {Object.entries(groups).map(([key, files]) => {
        if (key === "cotyledon" && !hasCotyledonCookie) {
          return null;
        }
        if (key === "years" || key === "cotyledon") {
          files.sort((a, b) => {
            return b.basename.localeCompare(a.basename);
          });
        } else {
          files.sort((a, b) => {
            return a.basename.localeCompare(b.basename);
          });
        }
        return (
          <div class="group" data-group={key}>
            <h3 style={`color:${colorMap[key as keyof typeof colorMap]};`}>
              {key}
            </h3>
            <ul>
              {files.map((file) => (
                <ListItem
                  file={file}
                  active={activeFilename === file.basename}
                  noDate={key === "cotyledon"}
                />
              ))}
            </ul>
          </div>
        );
      })}
    </div>
  );

  if (readme && isLast) {
    return (
      <div class="hsplit">
        <ReadmeView
          file={readme}
          siblingFiles={children}
          extra={!hasCotyledonCookie && (
            <>
              <a href="/file/cotyledon" class="custom cotyledon-link desktop">
                cotyledon
              </a>
            </>
          )}
        />
        {main}
        {!hasCotyledonCookie && (
          <>
            <a href="/file/cotyledon" class="custom cotyledon-link mobile">
              cotyledon
            </a>
          </>
        )}
      </div>
    );
  }

  return main;
}

function TextView({
  file,
  siblingFiles = [],
}: {
  file: MediaFile;
  siblingFiles?: MediaFile[];
}) {
  const contents = file.contents;

  if (file.path.startsWith("/2021/phoenix-write/maps")) {
    const basename = file.basename;
    if (basename.includes("map") && basename.endsWith(".txt")) {
      return (
        <pre
          style="white-space:normal;max-width:73ch"
          dangerouslySetInnerHTML={{
            __html: highlightHashComments(contents),
          }}
        ></pre>
      );
    }
  }

  return (
    <pre
      style="white-space:pre-wrap;max-width:70ch"
      dangerouslySetInnerHTML={{
        __html: highlightLinksInTextView(
          contents,
          siblingFiles.filter((f) => f.kind === MediaFileKind.file),
        ),
      }}
    ></pre>
  );
}

function ChatView({ file }: { file: MediaFile }) {
  const contents = file.contents;
  return (
    <div
      style="max-width:70ch"
      class="convo"
      dangerouslySetInnerHTML={{
        __html: highlightConvo(contents),
      }}
    >
    </div>
  );
}

function CodeView({ file }: { file: MediaFile }) {
  const highlightedContents = file.contents;
  if (!highlightedContents) {
    if (file.size > 1_000_000) {
      return <DownloadViewTooBig file={file} />;
    }
    return <DownloadViewCodeNotComputed file={file} />;
  }
  return (
    <pre
      style="white-space:pre-wrap"
      dangerouslySetInnerHTML={{
        __html: highlightedContents,
      }}
    />
  );
}

function VideoView({ file }: { file: MediaFile }) {
  addScript("@/tags/hls-polyfill.client.ts");
  const dimensions = file.parseDimensions() ?? { width: 1920, height: 1080 };
  return (
    <>
      <video
        controls
        preload
        style={`background:transparent;aspect-ratio:${
          simplifyFraction(
            dimensions.width,
            dimensions.height,
          )
        }`}
      >
        <source
          src={"/file" + file.path}
          type={mime.contentTypeFor(file.path)}
        />
      </video>
    </>
  );
}

function AudioView({
  file,
  onlyAudio = false,
}: {
  file: MediaFile;
  onlyAudio?: boolean;
}) {
  const parent = file.getParent();
  let lyricsFile: MediaFile | null = null;
  if (parent && !onlyAudio) {
    const siblings = parent.getPublicChildren();
    // show lyrics only if
    // - this media file is the only audio file
    // - there is lyrics.txt in the same directory
    const audioFiles = siblings.filter(
      (f) =>
        f.kind === MediaFileKind.file &&
        extensionToViewer[path.extname(f.basename)] === AudioView,
    );
    if (
      audioFiles.length === 1 &&
      siblings.find((f) => f.basename === "lyrics.txt")
    ) {
      lyricsFile = siblings.find((f) => f.basename === "lyrics.txt")!;
    }
  }
  return (
    <>
      <audio controls preload="none" style="width:100%;">
        <source
          src={"/file" + file.path}
          type={`audio/${path.extname(file.path).substring(1)}`}
        />
      </audio>
      {lyricsFile && (
        <div class="lyrics">
          <h2 style="margin-left:1rem;margin-bottom:-0.2rem;margin-top:0.5rem;">
            lyrics
          </h2>
          <TextView file={lyricsFile} siblingFiles={[]} />
        </div>
      )}
    </>
  );
}

function ImageView({ file }: { file: MediaFile }) {
  return (
    <img
      src={"/file" + file.path}
      alt={file.basename}
      style="max-width:100%;max-height:100%;object-fit:contain;margin:auto;display:block;"
    />
  );
}

function IframeView({ file }: { file: MediaFile }) {
  return (
    <>
      <iframe
        style="flex:1;height:100%;border:none"
        src={"/file" + file.path + "?view=embed"}
      />
    </>
  );
}

const helptexts: Record<string, any> = {
  ".blend": (
    <p>
      <code>.blend</code> files can be open in{" "}
      <a href="https://www.blender.org" target="_blank">
        Blender
      </a>
      .
    </p>
  ),
  ".comp": <FusionHelp ext="comp" name="composition files (project/scene)" />,
  ".setting": <FusionHelp ext="setting" name="macro/template" />,
  ".fuse": <FusionHelp ext="setting" name="FUSE Plugin (lua source code)" />,
};

function FusionHelp({ ext, name }: { ext: string; name: string }) {
  return (
    <>
      <p>
        <code>.{ext}</code> files are{" "}
        <a
          href="https://www.blackmagicdesign.com/products/fusion"
          target="_blank"
        >
          Fusion
        </a>{" "}
        {name}.
      </p>
      <p>
        Blackmagic Fusion is paid-only since mid-2019 (the "Studio" version).
        The last free version is Fusion 9.0.2, which exists for MacOS, Linux,
        and Microsoft Windows on{" "}
        <a href="https://www.blackmagicdesign.com/support" target="_blank">
          Blackmagic Design's support page
        </a>
        .
      </p>
      <p>
        Alternatively, you can use DaVinci Resolve, however I do not recommend
        it as it is very unstable, at least as of when I last tried version 18.
      </p>
    </>
  );
}
function DownloadView({ file }: { file: MediaFile }) {
  const help = helptexts[path.extname(file.basename)];
  return help
    ? (
      <>
        {help}
        <p>
          File download:{" "}
          <a href={"/file" + file.path + "?dl"} aria-label="download">
            {file.basename}
          </a>
          .
        </p>
      </>
    )
    : (
      <>
        <p>
          No built-in viewer for <code>{path.extname(file.basename)}</code>{" "}
          files.
        </p>
        <p>
          Instead, you may download it:{" "}
          <a href={"/file" + file.path + "?dl"} aria-label="download">
            {file.basename}
          </a>
          .
        </p>
      </>
    );
}

function DownloadViewTooBig({ file }: { file: MediaFile }) {
  return (
    <div style="padding:0.5rem">
      <p>
        This file is too large to view in the browser right now (
        <code>{formatSize(file.size)}</code>). Later, this file viewer will
        properly process video files for web streaming.
      </p>
      <p>
        Instead, you may download it:{" "}
        <a href={"/file" + file.path + "?dl"} aria-label="download">
          {file.basename}
        </a>
        .
      </p>
    </div>
  );
}

function DownloadViewCodeNotComputed({ file }: { file: MediaFile }) {
  return (
    <div style="padding:0.5rem">
      <p>
        This file has not been pre-processed for code highlighting. This either
        means it is not a valid UTF-8 text file (<code>.ts</code>{" "}
        can be TypeScript or an MPEG "Transport Stream"), or there is a bug in
        the file indexing logic.
      </p>
      <p>
        Instead, you may download it:{" "}
        <a href={"/file" + file.path + "?dl"} aria-label="download">
          {file.basename}
        </a>
        .
      </p>
    </div>
  );
}

function ReadmeView({
  file,
  siblingFiles,
  extra,
}: {
  file: MediaFile;
  siblingFiles: MediaFile[];
  extra?: any;
}) {
  // let showFileCandidates = siblingFiles.filter(f =>
  //   f.kind === MediaFileKind.file
  //   && (extensionToViewer[path.extname(f.basename)] === AudioView
  //     || extensionToViewer[path.extname(f.basename)] === VideoView)
  // );
  return (
    <div class="content readme">
      {/* {showFileCandidates.length === 1 && <AudioView file={showFileCandidates[0]} onlyAudio />} */}
      <TextView file={file} siblingFiles={siblingFiles} />
      {extra}
    </div>
  );
}

const gcd = (a: number, b: number): number => (b ? gcd(b, a % b) : a);
function simplifyFraction(n: number, d: number) {
  const divisor = gcd(n, d);
  return `${n / divisor}/${d / divisor}`;
}

// Add class properties to the components
AudioView.class = "audio";
ImageView.class = "image";
VideoView.class = "video";
TextView.class = "text";
DownloadView.class = "download";
CodeView.class = "code";

import "./clofi.css";
import assert from "node:assert";
import path, { dirname } from "node:path";
import { MediaFile, MediaFileKind } from "@/file-viewer/models/MediaFile.ts";
import { addScript } from "#sitegen";
import {
  escapeUri,
  formatDuration,
  formatSize,
  highlightConvo,
  highlightHashComments,
  highlightLinksInTextView,
} from "@/file-viewer/format.ts";
import { ForEveryone } from "@/file-viewer/cotyledon.tsx";
import * as mime from "#sitegen/mime";
