Browse Source

refactored timer.

Tomi Cvetic 4 years ago
parent
commit
5415da7e58

+ 2 - 0
frontend/initial-data.ts

@@ -46,6 +46,7 @@ const data: { trainings: ITraining[]; polls: any } = {
                 {
                   id: "block0",
                   duration: 40,
+                  video: "/media/block0.mp4",
                   exercises: [
                     {
                       id: "exercise0",
@@ -110,6 +111,7 @@ const data: { trainings: ITraining[]; polls: any } = {
                 {
                   id: "block1",
                   duration: 30,
+                  video: "/media/block1.webm",
                   exercises: [
                     {
                       id: "exercise1",

+ 19 - 0
frontend/package-lock.json

@@ -1314,6 +1314,25 @@
         "@babel/types": "^7.3.0"
       }
     },
+    "@types/cheerio": {
+      "version": "0.22.17",
+      "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.17.tgz",
+      "integrity": "sha512-izlm+hbqWN9csuB9GSMfCnAyd3/57XZi3rfz1B0C4QBGVMp+9xQ7+9KYnep+ySfUrCWql4lGzkLf0XmprXcz9g==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@types/enzyme": {
+      "version": "3.10.5",
+      "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.5.tgz",
+      "integrity": "sha512-R+phe509UuUYy9Tk0YlSbipRpfVtIzb/9BHn5pTEtjJTF5LXvUjrIQcZvNyANNEyFrd2YGs196PniNT1fgvOQA==",
+      "dev": true,
+      "requires": {
+        "@types/cheerio": "*",
+        "@types/react": "*"
+      }
+    },
     "@types/howler": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.1.2.tgz",

+ 1 - 0
frontend/package.json

@@ -44,6 +44,7 @@
     "@babel/preset-react": "^7.7.4",
     "@testing-library/react": "^9.4.0",
     "@testing-library/react-hooks": "^3.2.1",
+    "@types/enzyme": "^3.10.5",
     "@types/react": "^16.9.17",
     "@types/yup": "^0.26.27",
     "@zeit/next-typescript": "^1.1.1",

+ 0 - 0
frontend/pages/index.js → frontend/pages/index.tsx


+ 6 - 5
frontend/src/app/components/Footer.tsx

@@ -1,16 +1,17 @@
-import theme from '../../../src/styles/theme'
+import theme from "../../../src/styles/theme";
 
 const Footer = () => (
   <footer>
-    <p>Footer</p>
+    <p>u-fit &copy; 2020 Tomi Cvetic</p>
     <style jsx>{`
       footer {
+        text-align: center;
         background-color: ${theme.colors.darkgrey};
-        min-height: 125px;
+        min-height: 75px;
         color: ${theme.colors.offWhite};
       }
     `}</style>
   </footer>
-)
+);
 
-export default Footer
+export default Footer;

+ 3 - 3
frontend/src/app/components/Header.tsx

@@ -1,9 +1,9 @@
-import Logo from './Logo'
+import Logo from "./Logo";
 
 const Header = () => (
   <header>
     <Logo />
   </header>
-)
+);
 
-export default Header
+export default Header;

+ 23 - 32
frontend/src/app/components/Logo.tsx

@@ -1,38 +1,29 @@
 const Logo = () => (
-  <div id='logo'>
-    <div id='circle'>
-      <span id='circle-text'>˙u</span>
-    </div>
-    <span>fit</span>
+  <div id="logo">
+    <span id="circle">˙u</span>
+    <span id="text">fit</span>
 
-    <style jsx>{`
-      #logo {
-        position: relative;
-        font-size: 40px;
-        font-weight: 900;
-      }
+    <style jsx>
+      {`
+        #logo {
+          position: relative;
+          height: 60px;
+          width: 60px;
+          color: white;
+          background-color: red;
+          border-radius: 30px;
+          font-size: 40px;
+          font-weight: 900;
+          padding: 10px 0 0 15px;
+        }
 
-      #circle {
-        background-color: red;
-        color: white;
-        width: 60px;
-        height: 60px;
-        border-radius: 30px;
-      }
-
-      span {
-        position: absolute;
-        bottom: 0.1em;
-        left: 60px;
-      }
-
-      #circle-text {
-        right: 0.1em;
-        left: auto;
-      }
-    `}
+        #text {
+          color: black;
+          margin-left: 3px;
+        }
+      `}
     </style>
   </div>
-)
+);
 
-export default Logo
+export default Logo;

+ 3 - 0
frontend/src/timer/__tests__/__snapshots__/hooks.test.tsx.snap

@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`timer hook renders without error 1`] = `"<div><p id=\\"time\\">0</p><p id=\\"state\\">stopped</p><button id=\\"start\\"></button><button id=\\"stop\\"></button><button id=\\"reset\\"></button><button id=\\"setTime\\"></button></div>"`;

+ 44 - 0
frontend/src/timer/__tests__/hooks.test.tsx

@@ -0,0 +1,44 @@
+import { mount } from "enzyme";
+import { useTimer } from "../hooks";
+
+const TestApp = () => {
+  const [time, handler] = useTimer();
+  return (
+    <div>
+      <p id="time">{time}</p>
+      <p id="state">{handler.running ? "running" : "stopped"}</p>
+      <button id="start" onClick={handler.start}></button>
+      <button id="stop" onClick={handler.stop}></button>
+      <button id="reset" onClick={handler.reset}></button>
+      <button id="setTime" onClick={() => handler.setTime(314)}></button>
+    </div>
+  );
+};
+
+describe("timer hook", () => {
+  const container = mount(<TestApp />);
+  it("renders without error", () => {
+    expect(container.html()).toMatchSnapshot();
+    expect(container.find("p").length).toEqual(2);
+    expect(container.find("button").length).toEqual(4);
+  });
+  it("starts the timer", () => {
+    container.find("#start").simulate("click");
+    expect(container.find("#state").prop("children")).toEqual("running");
+  });
+
+  it("stops the timer", () => {
+    container.find("#stop").simulate("click");
+    expect(container.find("#state").prop("children")).toEqual("stopped");
+  });
+
+  it("sets the timer", () => {
+    container.find("#setTime").simulate("click");
+    expect(container.find("#time").prop("children")).toEqual(314);
+  });
+
+  it("resets the timer", () => {
+    container.find("#reset").simulate("click");
+    expect(container.find("#time").prop("children")).toEqual(0);
+  });
+});

+ 42 - 32
frontend/src/timer/__tests__/utils.test.ts

@@ -12,80 +12,90 @@ describe("finds the right position in the exercise.", () => {
   it("can handle negative numbers.", () => {
     const result = getPosition(exercises, -10);
     expect(result).toEqual({
-      exerciseIndex: 0,
-      currentTime: -10,
+      previousExercise: null,
+      currentExercise: exercises[0],
+      nextExercise: exercises[1],
       exerciseTime: 0
     });
   });
-  it("can find the first exercise.", () => {
+  it("can find the low first exercise.", () => {
     const result = getPosition(exercises, 0);
     expect(result).toEqual({
-      exerciseIndex: 0,
-      currentTime: 0,
+      previousExercise: null,
+      currentExercise: exercises[0],
+      nextExercise: exercises[1],
       exerciseTime: 0
     });
   });
-  it("can find the first exercise.", () => {
+  it("can find the high first exercise.", () => {
     const result = getPosition(exercises, 0.99);
     expect(result).toEqual({
-      exerciseIndex: 0,
-      currentTime: 1,
-      exerciseTime: 1
+      previousExercise: null,
+      currentExercise: exercises[0],
+      nextExercise: exercises[1],
+      exerciseTime: 0.99
     });
   });
-  it("can find the second exercise.", () => {
+  it("can find the low second exercise.", () => {
     const result = getPosition(exercises, 1);
     expect(result).toEqual({
-      exerciseIndex: 1,
-      currentTime: 1,
+      previousExercise: exercises[0],
+      currentExercise: exercises[1],
+      nextExercise: exercises[2],
       exerciseTime: 0
     });
   });
-  it("can find the second exercise.", () => {
+  it("can find the high second exercise.", () => {
     const result = getPosition(exercises, 2.99);
     expect(result).toEqual({
-      exerciseIndex: 1,
-      currentTime: 3,
-      exerciseTime: 2
+      previousExercise: exercises[0],
+      currentExercise: exercises[1],
+      nextExercise: exercises[2],
+      exerciseTime: 2.99 - 1
     });
   });
-  it("can find the third exercise.", () => {
+  it("can find the low third exercise.", () => {
     const result = getPosition(exercises, 3);
     expect(result).toEqual({
-      exerciseIndex: 2,
-      currentTime: 3,
+      previousExercise: exercises[1],
+      currentExercise: exercises[2],
+      nextExercise: exercises[3],
       exerciseTime: 0
     });
   });
-  it("can find the third exercise.", () => {
+  it("can find the high third exercise.", () => {
     const result = getPosition(exercises, 5.99);
     expect(result).toEqual({
-      exerciseIndex: 2,
-      currentTime: 6,
-      exerciseTime: 3
+      previousExercise: exercises[1],
+      currentExercise: exercises[2],
+      nextExercise: exercises[3],
+      exerciseTime: 5.99 - 3
     });
   });
-  it("can find the fourth exercise.", () => {
+  it("can find the low fourth exercise.", () => {
     const result = getPosition(exercises, 6);
     expect(result).toEqual({
-      exerciseIndex: 3,
-      currentTime: 6,
+      previousExercise: exercises[2],
+      currentExercise: exercises[3],
+      nextExercise: null,
       exerciseTime: 0
     });
   });
-  it("can find the fourth exercise.", () => {
+  it("can find the high fourth exercise.", () => {
     const result = getPosition(exercises, 9.99);
     expect(result).toEqual({
-      exerciseIndex: 3,
-      currentTime: 10,
-      exerciseTime: 4
+      previousExercise: exercises[2],
+      currentExercise: exercises[3],
+      nextExercise: null,
+      exerciseTime: 9.99 - 6
     });
   });
   it("can find the fourth exercise.", () => {
     const result = getPosition(exercises, 10);
     expect(result).toEqual({
-      exerciseIndex: null,
-      currentTime: 10,
+      previousExercise: null,
+      currentExercise: null,
+      nextExercise: null,
       exerciseTime: 0
     });
   });

+ 18 - 26
frontend/src/timer/components/Countdown.tsx

@@ -1,47 +1,33 @@
 import { formatTime } from "../../training/utils";
 import { useEffect, useState, useRef } from "react";
 import { Howl } from "howler";
-import { describeArc } from "../utils";
+import { describeArc, limit } from "../utils";
 
 interface ICountdown {
   seconds: number;
   totalPercent: number;
   exercisePercent: number;
+  onClick: () => void;
 }
 
-const Countdown = ({ seconds, totalPercent, exercisePercent }: ICountdown) => {
+const Countdown = ({
+  seconds,
+  totalPercent,
+  exercisePercent,
+  onClick
+}: ICountdown) => {
   const [color, setColor] = useState("rgba(55,55,55,1)");
   const [intSeconds, setIntSeconds] = useState(0);
 
   useEffect(() => {
-    setIntSeconds(Math.floor(seconds));
+    setIntSeconds(Math.ceil(seconds));
   }, [seconds]);
 
-  const rosie = useRef(
-    new Howl({
-      src: ["/media/ROSIE.mp3"],
-      sprite: {
-        five: [10211, 10883 - 10211],
-        four: [11292, 11873 - 11292],
-        three: [12251, 12809 - 12251],
-        two: [13208, 13930 - 13208],
-        one: [14376, 14948 - 14376]
-      }
-    })
-  );
-
   useEffect(() => {
     if (intSeconds <= 5) {
       fadeIn();
       setTimeout(fadeOut, 300);
     }
-    if (rosie.current) {
-      if (intSeconds === 5) rosie.current.play("five");
-      else if (intSeconds === 4) rosie.current.play("four");
-      else if (intSeconds === 3) rosie.current.play("three");
-      else if (intSeconds === 2) rosie.current.play("two");
-      else if (intSeconds === 1) rosie.current.play("one");
-    }
   }, [intSeconds]);
 
   function fadeIn() {
@@ -52,7 +38,7 @@ const Countdown = ({ seconds, totalPercent, exercisePercent }: ICountdown) => {
   }
 
   return (
-    <div id="timer" style={{ color }}>
+    <div id="timer" style={{ color }} onClick={onClick}>
       <svg>
         <text
           textAnchor="middle"
@@ -80,13 +66,19 @@ const Countdown = ({ seconds, totalPercent, exercisePercent }: ICountdown) => {
           strokeWidth="5"
         />
         <path
-          d={describeArc(150, 150, 140, 0, totalPercent * 360)}
+          d={describeArc(150, 150, 140, 0, limit(totalPercent * 360, 0, 359))}
           fill="none"
           stroke="#aa3333ff"
           strokeWidth="8"
         />
         <path
-          d={describeArc(150, 150, 130, 0, exercisePercent * 360)}
+          d={describeArc(
+            150,
+            150,
+            130,
+            0,
+            limit(exercisePercent * 360, 0, 359)
+          )}
           fill="none"
           stroke="#3333aaff"
           strokeWidth="8"

+ 45 - 57
frontend/src/timer/components/Timer.tsx

@@ -9,20 +9,12 @@ import VideoPlayer from "./VideoPlayer";
 import AudioPlayer from "./AudioPlayer";
 import { IExerciseItem } from "../types";
 import { Howl } from "howler";
+import { useTimer } from "../hooks";
 
 const Timer = ({ training }: { training: ITraining }) => {
-  const tickPeriod = 100;
+  const [time, timer] = useTimer({ tickPeriod: 100 });
 
-  const timeout: {
-    current: ReturnType<typeof setTimeout> | undefined;
-  } = useRef();
-
-  const [time, setTime] = useState(0);
   const [state, setState] = useState({
-    running: false,
-    startTime: 0,
-    stopTime: 0,
-    timeBuffer: 0,
     exerciseList: [] as IExerciseItem[],
     totalTime: 0
   });
@@ -31,20 +23,26 @@ const Timer = ({ training }: { training: ITraining }) => {
     new Howl({
       src: ["/media/ROSIE.mp3"],
       sprite: {
-        ttt: [114, 2956]
+        ttt: [114, 2956 - 114],
+        five: [10211, 10883 - 10211],
+        four: [11292, 11873 - 11292],
+        three: [12251, 12809 - 12251],
+        two: [13208, 13930 - 13208],
+        one: [14376, 14948 - 14376],
+        go: [18219, 18735 - 18219],
+        rest: [18988, 19615 - 18988]
       }
     })
   );
 
   useEffect(() => {
     //console.log("effect 1");
+    rosie.current.play("ttt");
     const exerciseList = getExerciseList(training.blocks);
     const totalTime = getTrainingTime(exerciseList);
     setState({ ...state, exerciseList, totalTime });
-    rosie.current.play("ttt");
   }, [training]);
 
-  const trainingOver = time >= state.totalTime;
   //console.log("is it over?", time, state.totalTime, trainingOver);
 
   const {
@@ -52,46 +50,40 @@ const Timer = ({ training }: { training: ITraining }) => {
     previousExercise,
     nextExercise,
     exerciseTime
-  } = getPosition(state.exerciseList, time);
-
-  const video: { current: VideoJsPlayer | undefined } = useRef();
-  //const audio: { current: Howl | undefined } = useRef();
+  } = getPosition(state.exerciseList, timer.time);
+  //console.log("aaa", time, currentExercise);
 
   useEffect(() => {
-    //console.log("effect 2", time);
-    if (!state.running) return;
-    timeout.current = setTimeout(tick, tickPeriod - state.timeBuffer);
-    setState({ ...state, timeBuffer: 0 });
-  }, [time, state.running]);
-
-  function tick() {
-    setTime((Date.now() - state.startTime) / 1000);
+    const countdown = currentExercise
+      ? currentExercise.duration + currentExercise.offset - timer.intTime + 1
+      : 0;
+    if (timer.running && rosie.current) {
+      if (countdown === 5) rosie.current.play("five");
+      else if (countdown === 4) rosie.current.play("four");
+      else if (countdown === 3) rosie.current.play("three");
+      else if (countdown === 2) rosie.current.play("two");
+      else if (countdown === 1) rosie.current.play("one");
+      if (exerciseTime < 1) {
+        if (currentExercise && currentExercise.exercise === "Rest")
+          rosie.current.play("rest");
+        else rosie.current.play("go");
+      }
+    }
+  }, [time, timer.running]);
 
-    if (trainingOver) stopTimer();
-  }
+  const video: { current: VideoJsPlayer | undefined } = useRef();
+  //const audio: { current: Howl | undefined } = useRef();
 
   function startTimer() {
-    if (trainingOver) return;
-    setState({
-      ...state,
-      running: true,
-      startTime: state.startTime + Date.now() - state.stopTime
-    });
+    if (time >= state.totalTime) return;
+    timer.start();
     if (video.current) video.current.play();
     //if (audio.current) audio.current.play();
     //console.log("Timer started.");
   }
 
   function stopTimer() {
-    if (timeout.current) clearTimeout(timeout.current);
-
-    setState({
-      ...state,
-      running: false,
-      stopTime: Date.now(),
-      timeBuffer: Date.now() - state.startTime
-    });
-
+    timer.stop();
     if (video.current) video.current.pause();
     //if (audio.current) audio.current.pause();
     //console.log("stopped");
@@ -99,21 +91,13 @@ const Timer = ({ training }: { training: ITraining }) => {
 
   function forward() {
     if (nextExercise) {
-      setState({
-        ...state,
-        startTime: state.startTime + 1000 * (time - nextExercise.offset)
-      });
-      setTime((Date.now() - state.startTime) / 1000);
+      timer.setTime(nextExercise.offset);
     }
   }
 
   function back() {
     if (previousExercise) {
-      setState({
-        ...state,
-        startTime: state.startTime + 1000 * (time - previousExercise.offset)
-      });
-      setTime((Date.now() - state.startTime) / 1000);
+      timer.setTime(previousExercise.offset);
     }
   }
 
@@ -125,10 +109,8 @@ const Timer = ({ training }: { training: ITraining }) => {
         {/*<label htmlFor="rest">Rest</label>
         <input type="number" min="25" max="60" step="5" defaultValue="25" />*/}
         <button onClick={back}>back</button>
-        <button
-          onClick={state.running ? () => stopTimer() : () => startTimer()}
-        >
-          {state.running ? "stop" : "start"}
+        <button onClick={timer.running ? stopTimer : startTimer}>
+          {timer.running ? "stop" : "start"}
         </button>
         <button onClick={forward}>forward</button>
       </div>
@@ -137,10 +119,11 @@ const Timer = ({ training }: { training: ITraining }) => {
           seconds={
             currentExercise ? currentExercise.duration - exerciseTime : 0
           }
-          totalPercent={time / state.totalTime}
+          totalPercent={timer.time / state.totalTime}
           exercisePercent={
             exerciseTime / (currentExercise ? currentExercise.duration : 1)
           }
+          onClick={timer.running ? stopTimer : startTimer}
         />
         <div className="header">current exercise</div>
         <div className="exercise">
@@ -151,6 +134,11 @@ const Timer = ({ training }: { training: ITraining }) => {
           {nextExercise ? nextExercise.exercise : "😎"}
         </div>
         <VideoPlayer
+          src={
+            currentExercise && currentExercise.video
+              ? currentExercise.video
+              : ""
+          }
           getVideoHandle={(videoHandle: VideoJsPlayer) =>
             (video.current = videoHandle)
           }

+ 13 - 9
frontend/src/timer/components/VideoPlayer.tsx

@@ -2,24 +2,28 @@ import { useRef, useEffect } from "react";
 import videojs, { VideoJsPlayer } from "video.js";
 
 const VideoPlayer = ({
-  getVideoHandle
+  getVideoHandle,
+  src
 }: {
   getVideoHandle: (videoHandle: VideoJsPlayer) => void;
+  src: string;
 }) => {
   const videoPlayerRef = useRef();
+  const video: { current: VideoJsPlayer | undefined } = useRef();
+
   useEffect(() => {
-    const video = videojs(videoPlayerRef.current, {
-      //sources: [{ src: "/media/tomi.webm" }],
-      sources: [{ src: "/media/Pexels_Videos_2786540.mp4" }],
-      width: 320,
-      loop: true
-    });
-    getVideoHandle(video);
+    const tmp = videojs(videoPlayerRef.current, { width: 320, loop: true });
+    getVideoHandle(tmp);
+    video.current = tmp;
     return () => {
-      if (video) video.dispose();
+      if (tmp) tmp.dispose();
     };
   }, []);
 
+  useEffect(() => {
+    if (video.current) video.current.src(src);
+  }, [src]);
+
   return (
     <div data-vjs-player>
       <video ref={videoPlayerRef as any} className="video-js" />

+ 74 - 0
frontend/src/timer/hooks.ts

@@ -0,0 +1,74 @@
+import { useState, useEffect, useRef } from "react";
+
+interface ITimer {
+  tickPeriod?: number;
+}
+
+interface ITimerHandler {
+  running: boolean;
+  time: number;
+  intTime: number;
+  start: () => void;
+  stop: () => void;
+  reset: () => void;
+  setTime: (value: number) => void;
+}
+
+interface ITimeoutRef {
+  current: ReturnType<typeof setTimeout> | undefined;
+}
+
+export const useTimer = (args?: ITimer): [number, ITimerHandler] => {
+  const { tickPeriod } = { tickPeriod: 100, ...args };
+
+  const [running, setRunning] = useState(false);
+  const [time, setTime] = useState(0);
+  const [intTime, setIntTime] = useState(0);
+
+  const timeout: ITimeoutRef = useRef();
+  const lastTick = useRef(Date.now());
+  const timeBuffer = useRef(0);
+
+  useEffect(() => {
+    if (!running) return;
+    timeout.current = setTimeout(tick, tickPeriod - timeBuffer.current);
+    timeBuffer.current = 0;
+  }, [time, running]);
+
+  function tick() {
+    const elapsedTime = Date.now() - lastTick.current;
+    lastTick.current = Date.now();
+    const newTime = (time * 1000 + elapsedTime) / 1000;
+    setTime(newTime);
+    setIntTime(Math.ceil(newTime));
+  }
+
+  function start() {
+    console.log("start");
+    lastTick.current = Date.now();
+    setRunning(true);
+  }
+
+  function stop() {
+    console.log("stop");
+    if (timeout.current) clearTimeout(timeout.current);
+    timeBuffer.current = Date.now() - lastTick.current;
+    setRunning(false);
+  }
+
+  function reset() {
+    setTime(0);
+  }
+
+  const handler = {
+    running,
+    time,
+    intTime,
+    start,
+    stop,
+    reset,
+    setTime
+  };
+
+  return [intTime, handler];
+};

+ 1 - 0
frontend/src/timer/types.ts

@@ -2,4 +2,5 @@ export interface IExerciseItem {
   exercise: string;
   duration: number;
   offset: number;
+  video?: string;
 }

+ 10 - 2
frontend/src/timer/utils.ts

@@ -12,10 +12,12 @@ export function getPosition(exerciseList: IExerciseItem[], time: number) {
     exercise => time < exercise.offset + exercise.duration
   );
 
-  const previousExercise = index > 0 ? exerciseList[index - 1] : null;
+  const previousExercise = index >= 1 ? exerciseList[index - 1] : null;
   const currentExercise = index >= 0 ? exerciseList[index] : null;
   const nextExercise =
-    index < exerciseList.length - 2 ? exerciseList[index + 1] : null;
+    index >= 0 && index < exerciseList.length - 1
+      ? exerciseList[index + 1]
+      : null;
   const values = {
     currentExercise,
     nextExercise,
@@ -52,6 +54,7 @@ export function getExerciseList(
             subBlocks.push({
               exercise: "Rest",
               duration: block.rest,
+              video: block.video,
               offset
             });
             offset += block.rest;
@@ -68,6 +71,7 @@ export function getExerciseList(
             )
             .join(" - "),
           duration: calculateDuration(block),
+          video: block.video,
           offset
         };
         offset += calculateDuration(block);
@@ -114,3 +118,7 @@ export function describeArc(
 
   return arcString;
 }
+
+export function limit(value: number, min: number, max: number) {
+  return Math.min(Math.max(min, value), max);
+}

+ 1 - 0
frontend/src/training/types.ts

@@ -27,6 +27,7 @@ export interface IBlock {
   format?: IFormat;
   blocks?: IBlock[];
   exercises?: IExercise[];
+  video?: string;
 }
 
 export interface IFormat {}