diff --git a/packages/web-kitchensink-reactnext/public/binary.bin b/packages/web-kitchensink-reactnext/public/binary.bin new file mode 100644 index 0000000..b307cfb Binary files /dev/null and b/packages/web-kitchensink-reactnext/public/binary.bin differ diff --git a/packages/web-kitchensink-reactnext/public/code.py b/packages/web-kitchensink-reactnext/public/code.py new file mode 100644 index 0000000..06d9935 --- /dev/null +++ b/packages/web-kitchensink-reactnext/public/code.py @@ -0,0 +1,106 @@ +import os +import random +from PIL import Image, ImageDraw, ImageFont + +# set up the image folder and font file +image_folder = 'dogetemplates/' +font_file = 'impact.ttf' + +# get a list of all the image files in the folder +image_files = [os.path.join(image_folder, f) for f in os.listdir(image_folder) if os.path.isfile(os.path.join(image_folder, f))] + +# set up the text parameters + +text_color = (255, 255, 255) +text_outline_color = (0, 0, 0) +text_outline_width = 2 + +# load the text files +with open('strings.txt', 'r') as f: + string_lines = f.readlines() +with open('strings_toptext.txt', 'r') as f: + string_toptext_lines = f.readlines() +with open('strings_bottomtext.txt', 'r') as f: + string_bottomtext_lines = f.readlines() + +# create the output folder if it doesn't exist +output_folder = 'randomdoges' +if not os.path.exists(output_folder): + os.makedirs(output_folder) + +# generate a random meme +image_file = random.choice(image_files) +image = Image.open(image_file) +draw = ImageDraw.Draw(image) + +# scale the font size based on a 500x500 image +if image.width < 500 and image.height < 500: + font_size = int(40) +else: + font_size = int(40 * (min(image.width, image.height) / 500)) +font = ImageFont.truetype(font_file, font_size) + +top_text = random.choice(string_lines + string_toptext_lines).strip().upper() +bottom_text = random.choice(string_lines + string_bottomtext_lines).strip().upper() + +text_width, text_height = draw.textsize(top_text, font) +while text_width > image.width: + font_size -= 1 + font = ImageFont.truetype(font_file, font_size) + text_width, text_height = draw.textsize(top_text, font) + +x = (image.width - text_width) / 2 +y = 10 +draw.text((x, y), top_text, font=font, fill=text_color, stroke_width=text_outline_width, stroke_fill=text_outline_color) + +text_width, text_height = draw.textsize(bottom_text, font) +while text_width > image.width: + font_size -= 1 + font = ImageFont.truetype(font_file, font_size) + text_width, text_height = draw.textsize(bottom_text, font) + +x = (image.width - text_width) / 2 +y = image.height - text_height - 10 +draw.text((x, y), bottom_text, font=font, fill=text_color, stroke_width=text_outline_width, stroke_fill=text_outline_color) + +# set the image size +min_size = 225 +max_size = 960 +new_size = random.randint(min_size, max_size) +image = image.resize((new_size, new_size)) + +# wrap text to new lines if it goes out of bounds +max_width = int(image.width * 0.8) +top_text_lines = [] +bottom_text_lines = [] +for text, lines in [(top_text, top_text_lines), (bottom_text, bottom_text_lines)]: + words = text.split() + current_line = words[0] + for word in words[1:]: + if draw.textsize(current_line + ' ' + word, font=font)[0] < max_width: + current_line += ' ' + word + else: + lines.append(current_line) + current_line = word + lines.append(current_line) + +# draw the text on the image +text_y = 10 +for line in top_text_lines: + text_width, text_height = draw.textsize(line, font) + x = (image.width - text_width) / 2 + draw.text((x, text_y), line, font=font, fill=text_color, stroke_width=text_outline_width, stroke_fill=text_outline_color) + text_y += text_height + +text_y = image.height - 10 +for line in reversed(bottom_text_lines): + text_width, text_height = draw.textsize(line, font) + x = (image.width - text_width) / 2 + text_y -= text_height + draw.text((x, text_y), line, font=font, fill=text_color, stroke_width=text_outline_width, stroke_fill=text_outline_color) + +# save the meme with an incrementing file name in the output folder +file_num = len(os.listdir(output_folder)) +file_name = f"randomdoge{file_num:04d}.jpg" # 4-digit padded file number +output_path = os.path.join(output_folder, file_name) +image.save(output_path) \ No newline at end of file diff --git a/packages/web-kitchensink-reactnext/public/plaintext.txt b/packages/web-kitchensink-reactnext/public/plaintext.txt new file mode 100644 index 0000000..31cdd66 --- /dev/null +++ b/packages/web-kitchensink-reactnext/public/plaintext.txt @@ -0,0 +1,95 @@ +Law 1: Never Outshine the Master: Ensure that those above you always feel superior. Go out of your way to make your bosses look better and feel smarter than anyone else. Everyone is insecure, but an insecure boss can retaliate more strongly than others can. + +Law 2: Never Put too Much Trust in Friends, Learn How to Use Enemies: Keep a close eye on your friends — they get envious and will undermine you. If you co-opt an enemy, he’ll be more loyal than a friend because he’ll try harder to prove himself worthy of your trust. + +Law 3: Conceal Your Intentions: Always hide your true intentions. Create a smokescreen. If you keep people off-balance and in the dark, they can’t counter your efforts. + +Law 4: Always Say Less than Necessary: Say little and be ambiguous, leaving the meaning to others to interpret. The less you say, the more intimidating and powerful you are. + +Law 5: So Much Depends on Reputation — Guard It with Your Life: Nurture and guard your reputation because reputation is integral to power. With a strong reputation, you can influence and intimidate others. + +Law 6: Create an Air of Mystery: Be outrageous or create an aura of mystery. Any attention — positive or negative — is better than being ignored. Attention brings you wealth. + +Law 7: Get Others to Do the Work for You, but Always Take the Credit: Get others to do your work for you. Use their skill, time, and energy to further your ambitions while taking full credit. You’ll be admired for your efficiency. + +Law 8: Make Other People Come to You — Use Bait if Necessary: Make your opponent come to you. When you force others to act, you’re in control. Bait them, then attack. + +Law 9: Win Through Your Actions, Never Through Argument: Demonstrate your point rather than arguing. Arguing rarely changes anyone’s mind, but people believe what they see. They’re also less likely to be offended. + +Law 10: Infection: Avoid the Unhappy and Unlucky: Avoid miserable people. The perpetually miserable spread misery like an infection, and they’ll drown you in it. + +Law 11: Learn to Keep People Dependent on You: Make your superior dependent on you. The more she needs you, the more security and freedom you have to pursue your goals. + +Law 12: Use Selective Honesty and Generosity to Disarm Your Victim: Use honesty and generosity to disarm and distract others from your schemes. Even the most suspicious people respond to acts of kindness, leaving them vulnerable to manipulation. + +Law 13: When Asking for Help, Appeal to People’s Self-Interest, Never to their Mercy or Gratitude: When you need help from someone in a position of power, appeal to their self-interest. They’ll be glad to help if they’ll get something in return, and you’ll get what you want without seeming desperate or irritating. + +Law 14: Pose as a Friend, Work as a Spy: Be friendly, sympathetic, and interested to get people to reveal their deepest thoughts and feelings. When you know your opponent’s secrets, you can predict his behavior and control him. + +Law 15: Crush Your Enemy Totally: Crush your enemy completely. If you leave even one ember smoldering, it will eventually ignite. You can’t afford to be lenient. + +Law 16: Use Absence to Increase Respect and Honor: Once you’ve become well-known, don’t wear out your welcome. The more you’re seen and heard from, the more you cheapen your brand. + +Law 17: Keep Others in Suspended Terror: Cultivate an Air of Unpredictability: Throw others off balance and unnerve them with random, unpredictable acts. You’ll gain the upper hand. + +Law 18: Do Not Build Fortresses to Protect Yourself – Isolation is Dangerous: Never isolate yourself when under pressure. This cuts you off from information you need, and when real danger arises you won’t see it coming. + +Law 19: Know Who You’re Dealing With – Do Not Offend the Wrong Person: When attempting to deceive someone, know who you’re dealing with, so you don’t waste your time or stir up a hornets’ nest in reaction. + +Law 20: Do Not Commit to Anyone: Don’t commit to any side or cause except yourself. By maintaining your independence, you remain in control — others will vie for your attention. You also have the ability to pit the sides against each other. + +Law 21: Play a Sucker to Catch a Sucker – Seem Dumber Than Your Mark: Make your intended victims feel as though they’re smarter than you are, and they won’t suspect you of having ulterior motives. + +Law 22: Use the Surrender Tactic: Transform Weakness into Power: When you’re weaker, surrender rather than fighting for the sake of honor. This gives you time to build strength and undermine your victor. You’ll win in the end. + +Law 23: Concentrate Your Forces: Focus your resources and energies where you’ll have the most impact or get the most benefit. Otherwise, you’ll waste limited time and energy. + +Law 24: Play the Perfect Courtier: Learn the rules of the society you’re playing in, and follow them to avoid attracting unfavorable attention. This includes appearing like a team player and being careful about criticizing diplomatically. + +Law 25: Re-Create Yourself: Create a powerful image that stands out, rather than letting others define you. Change your appearance and emotions to suit the occasion. People who seem larger than life attract admiration and power. + +Law 26: Keep Your Hands Clean: You’ll inevitably make mistakes or need to take care of unpleasant problems. But keep your hands clean by finding others to do the dirty work, and scapegoats to blame. + +Law 27: Play on People’s Need to Believe to Create a Cultlike Following: Offer people something to believe in and someone to follow. Promise the world but keep it vague; whip up enthusiasm. People will respond to a desperate need for belonging. Followers line your pockets, and your opponents are afraid to rile them. + +Law 28: Enter Action with Boldness: When you act, do so boldly — and if you make mistakes, correct them with even greater boldness. Boldness brings admiration and power. + +Law 29: Plan All the Way to the End: Make detailed plans with a clear ending. Take into account all possible developments. Then don’t be tempted from your path. Otherwise, you risk being surprised and forced to react without time to think. + +Law 30: Make Your Accomplishments Seem Effortless: Make difficult feats seem effortless and you’ll inspire awe in others and seem powerful. By contrast, when you make too much of your efforts, your achievement will seem less impressive and you’ll lose respect. + +Law 31: Control the Options: Get Others to Play with the Cards You Deal: To deceive people, seem to give them a meaningful choice. But sharply limit their options to a few that work in your favor regardless of which they choose. Your victims will feel in control, but you’ll pull the strings. + +Law 32: Play to People’s Fantasies: Conjure up alluring fantasies in contrast to the gloomy realities of life, and people will flock to you. Spin the right tale and wealth and power will follow. + +Law 33: Discover Each Man’s Thumbscrew: Everyone has a weakness, a hole in his armor. Find it and it’s leverage that you can use to your advantage. + +Law 34: Be Royal in Your Own Fashion: Act Like a King to Be Treated Like One: Act like royalty and people will treat you that way. Project dignity and supreme confidence that you’re destined for great things, and others will believe it. + +Law 35: Master the Art of Timing: Anticipate the ebb and flow of power. Recognize when the time is right, and align yourself with the right side. Be patient and wait for your moment. Bad timing ends careers and ambitions. + +Law 36: Disdain Things You Cannot Have: Ignoring Them Is the Best Revenge: Sometimes it’s better to ignore things because reacting can make small problems worse, make you look bad, and give your enemy attention. + +Law 37: Create Compelling Spectacles: In addition to words, use visuals and symbols to underscore your power. What people see makes a greater impression on them than what they hear. + +Law 38: Think as You Like But Behave Like Others: Don’t make a show of being different, or people will think you look down on them and will retaliate against you. + +Law 39: Stir Up Waters to Catch Fish: Always stay calm and objective. When you get angry, you’ve lost control. But if you can make your enemies angry, you gain an advantage. + +Law 40: Despise the Free Lunch: Use money and generosity strategically to achieve your goals. Use gifts to build a reputation of generosity, and also to obligate people to you. + +Law 41: Avoid Stepping Into a Great Man’s Shoes: If you succeed a great leader or famous parent, find or create your own space to fill. Sharply separate from the past and set your own standards — or you’ll be deemed a failure for not being a clone of your predecessor. + +Law 42: Strike the Shepherd and the Sheep Will Scatter: Trouble in a group often starts with a single individual who stirs the pot. Stop them before others succumb to their influence. + +Law 43: Work on the Hearts and Minds of Others: Win others’ hearts and minds. Play on their emotions and weaknesses, and appeal to their self-interest. You’ll have them eating out of your hand, and they’ll be less likely to turn on you. + +Law 44: Disarm and Infuriate with the Mirror Effect: Seduce people by mirroring their emotions and interests; create the illusion that you share their values. They’ll be so grateful to be understood that they won’t notice your ulterior motives. + +Law 45: Preach the Need for Change, But Never Reform Too Much at Once: Talk change but move slowly. Evoke revered history and cloak your changes in familiar rituals. Too much change is unsettling and will spark backlash. + +Law 46: Never Appear Too Perfect: To forestall or mitigate envy, admit to a flaw or weakness, emphasize the role of luck, or downplay your talents. If you don’t recognize and nip envy in the bud, it will grow and the envious will work insidiously against you. + +Law 47: Do Not Go Past the Mark You Aimed For; In Victory, Learn When to Stop: When you’ve won, don’t let emotions push you past your goal. The moment of victory is dangerous because if you press your luck, you’ll blunder into something you haven’t planned for. + +Law 48: Assume Formlessness: Be flexible, fluid, and unpredictable — formless — so your opponents can’t get a fix on you and can’t figure out how to respond. \ No newline at end of file diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx index 67d2f52..e4e444b 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx @@ -8,17 +8,17 @@ import { } from '@/utils/numeral'; import theme from '@/styles/theme'; import {useMediaControls} from '../../hooks/interactive'; -import {useFileMetadata} from '@/categories/blob/react'; +import {useFileMetadata, useFileUrl} from '@/categories/blob/react'; import clsx from 'clsx'; -import {SpectrogramCanvas, WaveformCanvas} from '@/packages/react-wavesurfer'; +import {SpectrogramCanvas, WaveformCanvas} from '@modal-soft/react-wavesurfer'; import {Slider} from '@/categories/number/react'; import {KeyValueTable} from '@/categories/information/react'; type AudioFilePreviewDerivedComponent = HTMLAudioElement; -export interface AudioFilePreviewProps extends Omit, 'controls'> { - file?: File; +export interface AudioFilePreviewProps = Partial> extends Omit, 'controls'> { + file?: F; disabled?: boolean; enhanced?: boolean; } @@ -27,12 +27,15 @@ export const AudioFilePreview = React.forwardRef { - const { augmentedFile, error } = useFileMetadata({ + const { fileWithUrl } = useFileUrl({ file, + }); + const { fileWithMetadata, error } = useFileMetadata({ + file: fileWithUrl, augmentFunction: augmentAudioFile, }); const { @@ -62,7 +65,12 @@ export const AudioFilePreview = React.forwardRef { + setEnhanced(enhancedProp); + }, [enhancedProp]); + + if (!fileWithMetadata) { return null; } @@ -79,7 +87,7 @@ export const AudioFilePreview = React.forwardRef
{ - typeof augmentedFile.metadata?.previewUrl === 'string' + typeof fileWithMetadata.url === 'string' && (
+ Audio playback not supported. - Math.floor(Number(c) / 2)).join(' ')})`} - interact - // waveColor={`rgb(${theme.primary})`} - // barHeight={4} - // minPxPerSec={20000} - // hideScrollbar - // autoCenter - // autoScroll - /> - -
- - -
+ ref={mediaControllerRef} + data-testid="preview" + barWidth={1} + barGap={1} + waveColor={`rgb(${theme.primary})`} + cursorWidth={2} + minPxPerSec={20000} + hideScrollbar + autoCenter + autoScroll + /> +
+ + +
+ + )}
{enhanced && (
@@ -298,83 +311,83 @@ export const AudioFilePreview = React.forwardRef -
+
-
-
- - Controls - - -
-
+ + + + )}
); diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/BinaryFilePreview/index.tsx b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/BinaryFilePreview/index.tsx index e20afb3..f9ab8cb 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/BinaryFilePreview/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/BinaryFilePreview/index.tsx @@ -1,92 +1,155 @@ import * as React from 'react'; -import {BinaryFile, getMimeTypeDescription} from '@/utils/blob'; +import {augmentBinaryFile, getMimeTypeDescription} from '@/utils/blob'; import {formatFileSize, formatNumeral} from '@/utils/numeral'; +import {useFileMetadata, useFileUrl} from '@/categories/blob/react'; +import clsx from 'clsx'; +import {KeyValueTable} from '@/categories/information/react'; +import {BinaryDataCanvas} from '@modal-soft/react-binary-data-canvas'; -export interface BinaryFilePreviewProps { - file: BinaryFile; +type BinaryFilePreviewDerivedComponent = HTMLDivElement; + +export interface BinaryFilePreviewProps = Partial> extends React.HTMLProps { + file?: F; + enhanced?: boolean; } -export const BinaryFilePreview: React.FC = ({ - file: f, -}) => ( -
-
- { - f.metadata && (f.metadata?.contents instanceof ArrayBuffer) - && ( -
-
-              
-                {
-                  (Array.from(new Uint8Array((f.metadata.contents as ArrayBuffer).slice(0, 256))) as number[])
-                    .reduce(
-                      (byteArray: number[][], byte: number, i) => {
-                        if (i % 16 === 0) {
-                          return [
-                            ...byteArray,
-                            [byte],
-                          ]
-                        }
+export const BinaryFilePreview = React.forwardRef(({
+  file,
+  className,
+  style,
+  enhanced: enhancedProp = false,
+  ...etcProps
+}, forwardedRef) => {
+  const { fileWithUrl } = useFileUrl({ file });
+  const { fileWithMetadata, error } = useFileMetadata({
+    file: fileWithUrl,
+    augmentFunction: augmentBinaryFile,
+  });
 
-                        const lastLine = byteArray.at(-1) as number[]
+  const [enhanced, setEnhanced] = React.useState(false);
+  React.useEffect(() => {
+    setEnhanced(enhancedProp);
+  }, [enhancedProp]);
 
-                        return [
-                          ...(byteArray.slice(0, -1)),
-                          [...lastLine, byte],
-                        ]
-                      },
-                      [] as number[][],
-                    )
-                    .map((ba: number[]) => ba
-                      .map((a) => a.toString(16).padStart(2, '0'))
-                      .join(' ')
-                    )
-                    .join('\n')
-                }
-              
-            
-
- ) - } -
-
-
-
- Name -
-
- {f.name} -
-
-
-
- Type -
-
- {getMimeTypeDescription(f.type, f.name)} -
+ if (!fileWithMetadata) { + return null; + } + + return ( +
+
+
+ { + fileWithMetadata.metadata && (fileWithMetadata.metadata?.contents instanceof ArrayBuffer) + && ( +
+ { + if (byte < 0x10) { + return 'x00'; + } + if (byte < 0x20) { + return 'x10'; + } + if (byte < 0x30) { + return 'x20'; + } + if (byte < 0x40) { + return 'x30'; + } + if (byte < 0x50) { + return 'x40'; + } + if (byte < 0x60) { + return 'x50'; + } + if (byte < 0x70) { + return 'x60'; + } + if (byte < 0x80) { + return 'x70'; + } + if (byte < 0x90) { + return 'x80'; + } + if (byte < 0xA0) { + return 'x90'; + } + if (byte < 0xB0) { + return 'xa0'; + } + if (byte < 0xC0) { + return 'xb0'; + } + if (byte < 0xD0) { + return 'xc0'; + } + if (byte < 0xE0) { + return 'xd0'; + } + if (byte < 0xF0) { + return 'xe0'; + } + return 'xf0'; + }} + /> +
+ ) + } +
-
-
- Size -
-
- {formatFileSize(f.size)} -
+
+
-
-
-); +
+ ); +}); + +BinaryFilePreview.displayName = 'BinaryFilePreview'; diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/ImageFilePreview/index.tsx b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/ImageFilePreview/index.tsx index 1f77f2c..1992af6 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/ImageFilePreview/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/ImageFilePreview/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import {augmentImageFile, FileWithDataUrl, getMimeTypeDescription} from '@/utils/blob'; +import {augmentImageFile, getMimeTypeDescription} from '@/utils/blob'; import {formatFileSize, formatNumeral} from '@/utils/numeral'; import clsx from 'clsx'; import {useFileMetadata, useFileUrl, useImageControls} from '@/categories/blob/react'; @@ -7,9 +7,10 @@ import {KeyValueTable} from '@/categories/information/react'; type ImageFilePreviewDerivedComponent = HTMLImageElement; -export interface ImageFilePreviewProps extends Omit, 'src' | 'alt'> { - file?: Partial; +export interface ImageFilePreviewProps = Partial> extends Omit, 'src' | 'alt'> { + file?: F; disabled?: boolean; + enhanced?: boolean; } export const ImageFilePreview = React.forwardRef(({ @@ -17,10 +18,11 @@ export const ImageFilePreview = React.forwardRef { const { fileWithUrl, loading: urlLoading } = useFileUrl({ file }); - const { augmentedFile, loading: metadataLoading, error } = useFileMetadata({ + const { fileWithMetadata, loading: metadataLoading, error } = useFileMetadata({ file: fileWithUrl as File, augmentFunction: augmentImageFile, }); @@ -33,12 +35,17 @@ export const ImageFilePreview = React.forwardRef { + setEnhanced(enhancedProp); + }, [enhancedProp]); + + if (!(fileWithMetadata)) { return null; } const cannotDisplayPicture = Boolean( - typeof augmentedFile.url !== 'string' + typeof fileWithMetadata.url !== 'string' && error ); @@ -52,7 +59,7 @@ export const ImageFilePreview = React.forwardRef
- {typeof augmentedFile.url === 'string' && ( + {typeof fileWithMetadata.url === 'string' && ( @@ -82,47 +89,47 @@ export const ImageFilePreview = React.forwardRef ( + children: fileWithMetadata.metadata?.palette.map((rgb, i) => ( @@ -166,55 +173,57 @@ export const ImageFilePreview = React.forwardRef -
-
- - Controls - - - {' '} - + {' '} + -
-
+ + + + )}
); diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/TextFilePreview/index.tsx b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/TextFilePreview/index.tsx index f000680..c86f655 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/TextFilePreview/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/TextFilePreview/index.tsx @@ -1,106 +1,146 @@ import * as React from 'react'; -import Prism from 'prismjs'; import {formatFileSize, formatNumeral} from '@/utils/numeral'; -import {TextFile} from '@/utils/blob'; +import {useFileMetadata, useFileUrl} from '@/categories/blob/react'; +import {augmentTextFile, getMimeTypeDescription} from '@/utils/blob'; +import clsx from 'clsx'; +import {KeyValueTable} from '@/categories/information/react'; +import {Prism} from '@modal-soft/react-prism'; -export interface TextFilePreviewProps { - file: TextFile; +type TextFilePreviewDerivedComponent = HTMLDivElement; + +export interface TextFilePreviewProps = Partial> extends React.HTMLProps { + file?: F; + enhanced?: boolean; } -export const TextFilePreview: React.FC = ({ - file: f, -}) => ( -
-
- { - typeof f.metadata?.contents === 'string' - && ( -
-
-              {
-                typeof f.metadata.scheme === 'string'
-                && (
-                  
-                )
-              }
-              {
-                typeof f.metadata.scheme !== 'string'
-                && (
-                  
-                    {f.metadata.contents}
-                  
-                )
-              }
-            
-
- ) - } -
-
-
-
- Name -
-
- {f.name} -
-
-
-
- Type -
-
- {typeof f.metadata?.schemeTitle === 'string' ? `${f.metadata.schemeTitle} Source` : 'Text File'} -
+export const TextFilePreview = React.forwardRef(({ + file, + className, + style, + enhanced: enhancedProp = false, + ...etcProps +}, forwardedRef) => { + const { fileWithUrl } = useFileUrl({ file }); + const { fileWithMetadata, error } = useFileMetadata({ + file: fileWithUrl, + augmentFunction: augmentTextFile, + }); + + const [enhanced, setEnhanced] = React.useState(false); + React.useEffect(() => { + setEnhanced(enhancedProp); + }, [enhancedProp]); + + if (!fileWithMetadata) { + return null; + } + + return ( +
+
+
+ { + typeof fileWithMetadata.metadata?.contents === 'string' + && ( +
+ +
+ ) + } +
-
-
- Size -
-
- {formatFileSize(f.size)} -
+
+ + {fileWithMetadata.metadata!.languageMatches.slice(0, 3).map(([language, probability], index) => { + return ( +
+
+ {language.slice(0, 1).toUpperCase()} + {language.slice(1)} +
+
+ ({(probability * 100).toFixed(3)}%) +
+
+ ); + })} +
+ ), + }, + }, + typeof fileWithMetadata.metadata?.lineCount === 'number' + && typeof fileWithMetadata.metadata?.linesOfCode !== 'number' + && { + key: 'Lines', + valueProps: { + children: `${formatNumeral(fileWithMetadata.metadata.lineCount)} line(s)` + }, + }, + typeof fileWithMetadata.metadata?.lineCount === 'number' + && typeof fileWithMetadata.metadata?.linesOfCode === 'number' + && { + key: 'Lines', + valueProps: { + children: `${formatNumeral(fileWithMetadata.metadata.lineCount)} line(s), ${formatNumeral(fileWithMetadata.metadata.linesOfCode)} loc`, + }, + }, + ]} + />
- { - typeof f.metadata?.language === 'string' - && ( -
-
- Language -
-
- {f.metadata.language.slice(0, 1).toUpperCase()} - {f.metadata.language.slice(1)} -
-
- ) - } - -
-); + + ); +}); + +TextFilePreview.displayName = 'TextFilePreview'; diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx index b66a490..f4cd907 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx @@ -1,15 +1,15 @@ import * as React from 'react'; import {augmentVideoFile, getMimeTypeDescription} from '@/utils/blob'; import {formatFileSize, formatNumeral, formatSecondsDurationConcise} from '@/utils/numeral'; -import {useFileMetadata, useMediaControls} from '@tesseract-design/web-blob-react'; +import {useFileMetadata, useFileUrl, useMediaControls} from '@tesseract-design/web-blob-react'; import clsx from 'clsx'; import {Slider} from '@tesseract-design/web-number-react'; import {KeyValueTable} from '@/categories/information/react'; type VideoFilePreviewDerivedComponent = HTMLVideoElement; -export interface VideoFilePreviewProps extends Omit, 'controls'> { - file?: File; +export interface VideoFilePreviewProps = Partial> extends Omit, 'controls'> { + file?: F; disabled?: boolean; enhanced?: boolean; } @@ -19,11 +19,12 @@ export const VideoFilePreview = React.forwardRef { - const { augmentedFile, error } = useFileMetadata({ - file, + const { fileWithUrl } = useFileUrl({ file }); + const { fileWithMetadata, error } = useFileMetadata({ + file: fileWithUrl, augmentFunction: augmentVideoFile, }); const { @@ -50,7 +51,12 @@ export const VideoFilePreview = React.forwardRef { + setEnhanced(enhancedProp); + }, [enhancedProp]); + + if (!fileWithMetadata) { return null; } @@ -67,7 +73,7 @@ export const VideoFilePreview = React.forwardRef
{ - typeof augmentedFile.metadata?.previewUrl === 'string' + typeof fileWithMetadata.url === 'string' && (
- - + + + + ) + }
); diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/blob/metadata.ts b/packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/blob/metadata.ts index 702f22a..d6366a2 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/blob/metadata.ts +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/blob/metadata.ts @@ -35,9 +35,9 @@ export const useFileUrl = (options: UseFileUrlOptions) => { }), [fileWithUrl, loading]); }; -export interface UseFileMetadataOptions = Partial> { - file?: File; - augmentFunction: (file: File) => Promise; +export interface UseFileMetadataOptions = Partial, U extends Partial = Partial> { + file?: U; + augmentFunction: (file: U) => Promise; } export const useFileMetadata = >(options: UseFileMetadataOptions) => { @@ -67,7 +67,7 @@ export const useFileMetadata = >(options: UseFileMetadat }, [file, augmentFunction]); return React.useMemo(() => ({ - augmentedFile: fileWithMetadata, + fileWithMetadata, error, loading, }), [fileWithMetadata, loading, error]); diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/index.ts b/packages/web-kitchensink-reactnext/src/categories/blob/react/index.ts index fc6ee9b..544f9fb 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/index.ts +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/index.ts @@ -1,9 +1,9 @@ export * from './components/AudioFilePreview'; //export * from './components/AudioMiniFilePreview'; -//export * from './components/BinaryFilePreview'; +export * from './components/BinaryFilePreview'; //export * from './components/FileSelectBox'; export * from './components/ImageFilePreview'; -//export * from './components/TextFilePreview'; +export * from './components/TextFilePreview'; export * from './components/VideoFilePreview'; export * from './hooks/blob'; diff --git a/packages/web-kitchensink-reactnext/src/categories/information/react/components/KeyValueTable/index.tsx b/packages/web-kitchensink-reactnext/src/categories/information/react/components/KeyValueTable/index.tsx index 908588b..1f7744b 100644 --- a/packages/web-kitchensink-reactnext/src/categories/information/react/components/KeyValueTable/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/information/react/components/KeyValueTable/index.tsx @@ -22,7 +22,7 @@ export const KeyValueTable = React.forwardRef @@ -33,7 +33,7 @@ export const KeyValueTable = React.forwardRef
{property.key}
@@ -41,8 +41,8 @@ export const KeyValueTable = React.forwardRef diff --git a/packages/web-kitchensink-reactnext/src/utils/audio.ts b/packages/web-kitchensink-reactnext/src/packages/audio-utils/index.ts similarity index 67% rename from packages/web-kitchensink-reactnext/src/utils/audio.ts rename to packages/web-kitchensink-reactnext/src/packages/audio-utils/index.ts index 5f2eb93..ea57f6b 100644 --- a/packages/web-kitchensink-reactnext/src/utils/audio.ts +++ b/packages/web-kitchensink-reactnext/src/packages/audio-utils/index.ts @@ -1,6 +1,11 @@ import WaveSurfer from 'wavesurfer.js'; -export const getAudioMetadata = (audioUrl: string, fileType: string) => new Promise>(async (resolve, reject) => { +export interface AudioMetadata { + duration?: number; +} + +export const getAudioMetadata = (audioUrl?: string, fileType?: string) => new Promise(async (resolve, reject) => { + // TODO server side if (fileType === 'audio/mid') { resolve({}); return; @@ -22,6 +27,11 @@ export const getAudioMetadata = (audioUrl: string, fileType: string) => new Prom resolve(metadata); }); + if (audioUrl === undefined) { + resolve({}); + return; + } + await waveSurferInstance.load(audioUrl); } catch (err) { reject(err); diff --git a/packages/web-kitchensink-reactnext/src/utils/image.ts b/packages/web-kitchensink-reactnext/src/packages/image-utils/index.ts similarity index 78% rename from packages/web-kitchensink-reactnext/src/utils/image.ts rename to packages/web-kitchensink-reactnext/src/packages/image-utils/index.ts index 81f44e4..a14eb7a 100644 --- a/packages/web-kitchensink-reactnext/src/utils/image.ts +++ b/packages/web-kitchensink-reactnext/src/packages/image-utils/index.ts @@ -1,6 +1,15 @@ import ColorThief from 'colorthief'; -export const getImageMetadata = (imageUrl?: string) => new Promise>((resolve, reject) => { +type RgbTuple = [number, number, number]; + +export interface ImageMetadata { + width?: number; + height?: number; + palette?: RgbTuple[]; +} + +export const getImageMetadata = (imageUrl?: string) => new Promise((resolve, reject) => { + // TODO server side const image = new Image(); image.addEventListener('load', async (imageLoadEvent) => { diff --git a/packages/web-kitchensink-reactnext/src/packages/react-binary-data-canvas/index.tsx b/packages/web-kitchensink-reactnext/src/packages/react-binary-data-canvas/index.tsx new file mode 100644 index 0000000..af4fa26 --- /dev/null +++ b/packages/web-kitchensink-reactnext/src/packages/react-binary-data-canvas/index.tsx @@ -0,0 +1,117 @@ +import * as React from 'react'; +import clsx from 'clsx'; + +type BinaryDataCanvasDerivedElement = HTMLPreElement; + +export interface BinaryDataCanvasProps extends Omit, 'children' | 'headers'> { + arrayBuffer?: ArrayBuffer; + lineClassName?: string; + byteClassName?: string | ((byte: number, index: number) => string) + headers?: boolean; +} + +const BYTES_PER_LINE = 16 as const; + +export const BinaryDataCanvas = React.forwardRef(({ + arrayBuffer, + lineClassName, + byteClassName, + headers = false, + className, + ...etcProps +}, forwardedRef) => { + const bytesGrouped = arrayBuffer ? (Array.from(new Uint8Array(arrayBuffer)).slice(0, BYTES_PER_LINE * 20) as number[]) + .reduce( + (byteArray: number[][], byte: number, i) => { + if (i % BYTES_PER_LINE === 0) { + return [ + ...byteArray, + [byte], + ] + } + + const lastLine = byteArray.at(-1) as number[] + + return [ + ...(byteArray.slice(0, -1)), + [...lastLine, byte], + ] + }, + [] as number[][], + ) : [] as number[][]; + + return ( +
+ {headers && ( +
+          
+            {'\u00a0'}
+          
+          {bytesGrouped.map((_, i) => (
+            
+              {'\n'}
+              
+                0x{(BYTES_PER_LINE * i).toString(16).padStart(2, '0')}
+              
+            
+          ))}
+        
+ )} +
+
+          
+            {headers && new Array(BYTES_PER_LINE).fill(0).map((_, i) => (
+              
+                {i > 0 && ' '}
+                {i.toString(16).padStart(2, '+')}
+              
+            ))}
+            {
+              bytesGrouped.map((line: number[], l) => (
+                
+                  {l > 0 && '\n'}
+                  
+                    {line.map((byte, b) => (
+                      
+                        {b > 0 && ' '}
+                        
+                          {byte.toString(16).padStart(2, '0')}
+                        
+                      
+                    ))}
+                  
+                
+              ))
+            }
+          
+        
+
+
+ ) +}); + +BinaryDataCanvas.displayName = 'BinaryDataCanvas'; diff --git a/packages/web-kitchensink-reactnext/src/packages/react-prism/index.tsx b/packages/web-kitchensink-reactnext/src/packages/react-prism/index.tsx new file mode 100644 index 0000000..2c7369e --- /dev/null +++ b/packages/web-kitchensink-reactnext/src/packages/react-prism/index.tsx @@ -0,0 +1,134 @@ +import * as React from 'react'; +import PrismJs from 'prismjs'; +import clsx from 'clsx'; + +type PrismDerivedElement = HTMLPreElement; + +export interface PrismProps extends Omit, 'children'> { + code?: string; + language?: string; + lineNumbers?: boolean; + maxLineNumber?: number; + tabSize?: number; +} + +export const Prism = React.forwardRef(({ + code, + language = 'plain', + lineNumbers = false, + maxLineNumber = Infinity, + tabSize = 2, + className, + style, + ...etcProps +}, forwardedRef) => { + const [highlightedCode, setHighlightedCode] = React.useState(''); + + React.useEffect(() => { + const loadHighlighter = async () => { + if (language !== 'plain') { + await import(`prismjs/components/prism-${language}`); + } + + if (!code) { + setHighlightedCode(''); + return; + } + + if (!Number.isFinite(maxLineNumber)) { + setHighlightedCode( + PrismJs.highlight( + code, + PrismJs.languages[language], + language, + ) + ) + return; + } + + setHighlightedCode( + PrismJs.highlight( + code, + PrismJs.languages[language], + language, + ).split('\n').slice(0, maxLineNumber).join('\n') + ); + } + + void loadHighlighter(); + }, [language, code, maxLineNumber]); + + if (!code) { + return null; + } + + + return ( +
+ {lineNumbers && ( +
+          {new Array(maxLineNumber).fill(0).map((_, i) => (
+            
+              {i > 0 && '\n'}
+              
+                {i + 1}
+              
+            
+          ))}
+        
+ )} +
+        {
+          language !== 'plain'
+          && (
+            
+          )
+        }
+        {
+          language === 'plain'
+          && (
+            
+              {code}
+            
+          )
+        }
+      
+
+ ) +}); + +Prism.displayName = 'Prism'; + diff --git a/packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/WaveformCanvas/index.tsx b/packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/WaveformCanvas/index.tsx index 8bbf15f..e952fc0 100644 --- a/packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/WaveformCanvas/index.tsx +++ b/packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/WaveformCanvas/index.tsx @@ -80,6 +80,7 @@ export const WaveformCanvas = React.forwardRef
-
diff --git a/packages/web-kitchensink-reactnext/src/utils/text.ts b/packages/web-kitchensink-reactnext/src/packages/text-utils/index.ts similarity index 59% rename from packages/web-kitchensink-reactnext/src/utils/text.ts rename to packages/web-kitchensink-reactnext/src/packages/text-utils/index.ts index 3aa5452..bd020b3 100644 --- a/packages/web-kitchensink-reactnext/src/utils/text.ts +++ b/packages/web-kitchensink-reactnext/src/packages/text-utils/index.ts @@ -31,15 +31,40 @@ const RESOLVED_ALIASES = Object.fromEntries( ) ); -export const getTextMetadata = (contents: string, filename: string) => { +type Language = string; +type LanguageProbability = number; +type LanguageMatch = [Language, LanguageProbability]; + +export interface TextMetadata { + contents?: string; + scheme?: string; + schemeTitle?: string; + languageMatches?: LanguageMatch[]; + lineCount?: number; + linesOfCode?: number; +} + +const countLinesOfCode = (lines: string[], scheme: string): number => { + // TODO count loc depending on scheme + + //return lines.filter((line) => !line.trim().startsWith('//')).length; + + return lines.filter((line) => line.trim().length > 0).length; +} + +export const getTextMetadata = (contents: string, filename?: string): Promise => { + const lineNormalizedContents = contents.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + const lines = lineNormalizedContents.split('\n'); + const lineCount = lines.length; const metadata = Object.entries(RESOLVED_ALIASES).reduce( (theMetadata, [key, value]) => { - if (typeof theMetadata.scheme === 'undefined' && filename.endsWith(value.extension)) { + if (typeof theMetadata.scheme === 'undefined' && filename?.endsWith(value.extension)) { if (value.aliasOf) { return { ...theMetadata, scheme: value.aliasOf, schemeTitle: value.title, + linesOfCode: countLinesOfCode(lines, value.aliasOf), } } @@ -47,21 +72,22 @@ export const getTextMetadata = (contents: string, filename: string) => { ...theMetadata, scheme: key, schemeTitle: value.title, + linesOfCode: countLinesOfCode(lines, key), }; } return theMetadata; }, - {} as Record + { + contents, + lineCount, + } as TextMetadata ); if (typeof metadata.scheme !== 'string') { const naturalLanguageDetector = new LanguageDetect(); - const probableLanguages = naturalLanguageDetector.detect(contents); - const [languageName, probability] = probableLanguages[0]; - metadata.language = languageName; - metadata.languageProbability = probability; + metadata.languageMatches = naturalLanguageDetector.detect(contents); } - return metadata; + return Promise.resolve(metadata); } diff --git a/packages/web-kitchensink-reactnext/src/utils/video.ts b/packages/web-kitchensink-reactnext/src/packages/video-utils/index.ts similarity index 63% rename from packages/web-kitchensink-reactnext/src/utils/video.ts rename to packages/web-kitchensink-reactnext/src/packages/video-utils/index.ts index 961b236..cf8476c 100644 --- a/packages/web-kitchensink-reactnext/src/utils/video.ts +++ b/packages/web-kitchensink-reactnext/src/packages/video-utils/index.ts @@ -1,4 +1,11 @@ -export const getVideoMetadata = (videoUrl: string) => new Promise>((resolve, reject) => { +export interface VideoFileMetadata { + width?: number; + height?: number; + duration?: number; +} + +export const getVideoMetadata = (videoUrl?: string) => new Promise((resolve, reject) => { + // TODO server side const video = window.document.createElement('video'); const source = window.document.createElement('source'); @@ -7,6 +14,7 @@ export const getVideoMetadata = (videoUrl: string) => new Promise new Promise { }); }, []); + const [binaryFile, setBinaryFile] = React.useState(); + React.useEffect(() => { + fetch('/binary.bin').then((response) => { + response.blob().then((blob) => { + setBinaryFile(new File([blob], 'binary.bin', { + type: 'application/octet-stream', + })); + }); + }); + }, []); + + const [plaintextFile, setPlaintextFile] = React.useState(); + React.useEffect(() => { + fetch('/plaintext.txt').then((response) => { + response.blob().then((blob) => { + setPlaintextFile(new File([blob], 'plaintext.txt', { + type: 'text/plain', + })); + }); + }); + }, []); + + const [codeFile, setCodeFile] = React.useState(); + React.useEffect(() => { + fetch('/code.py').then((response) => { + response.blob().then((blob) => { + setCodeFile(new File([blob], 'code.py', { + type: 'text/x-python', + })); + }); + }); + }, []); + return (
} className="sm:h-64" /> @@ -61,7 +95,14 @@ const BlobPage: NextPage = () => {
+ } className="sm:h-64" enhanced /> @@ -70,12 +111,64 @@ const BlobPage: NextPage = () => {
+ } className="sm:h-64" enhanced />
+
+ + + } + className="sm:h-64" + /> + +
+
+ + + } + className="sm:h-64" + /> + +
+
+ + + } + className="sm:h-64" + /> + +
{/* { - metadata?: Record; -} +import {getTextMetadata, TextMetadata} from '@modal-soft/text-utils'; +import {getImageMetadata, ImageMetadata} from '@modal-soft/image-utils'; +import {AudioMetadata, getAudioMetadata} from '@modal-soft/audio-utils'; +import {getVideoMetadata, VideoFileMetadata} from '@modal-soft/video-utils'; const MIME_TYPE_DESCRIPTIONS = { 'image/gif': 'GIF Image', @@ -116,8 +111,6 @@ export const getContentType = (mimeType?: string, filename?: string) => { return ContentType.BINARY; } -export const readAsText = (blob: Partial) => blob?.text?.(); - export const readAsDataURL = (blob: Partial) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.addEventListener('error', () => { @@ -135,57 +128,6 @@ export const readAsDataURL = (blob: Partial) => new Promise((resol reader.readAsDataURL(blob as Blob); }); -export const readAsArrayBuffer = (blob: Blob) => blob.arrayBuffer(); - -interface FileWithResolvedType extends Partial { - resolvedType: T; - originalFile?: File; -} - -export interface TextFileMetadata { - contents?: string; - scheme?: string; - schemeTitle?: string; - language?: string; - languageProbability?: number; -} - -export interface TextFile extends FileWithResolvedType { - metadata?: TextFileMetadata; -} - -const augmentTextFile = async (f: File): Promise => { - const contents = await readAsText(f); - const metadata = getTextMetadata(contents ?? '', f.name) as TextFileMetadata; - return { - ...f, - name: f.name, - type: f.type, - size: f.size, - lastModified: f.lastModified, - resolvedType: ContentType.TEXT, - originalFile: f, - metadata: { - contents, - language: metadata.language, - languageProbability: metadata.languageProbability, - scheme: metadata.scheme, - schemeTitle: metadata.schemeTitle, - }, - }; -}; - -export interface ImageFileMetadata { - previewUrl?: string; - width?: number; - height?: number; - palette?: [number, number, number][]; -} - -export interface ImageFile extends FileWithResolvedType { - metadata?: ImageFileMetadata; -} - export interface FileWithDataUrl extends File { url?: string; } @@ -195,94 +137,72 @@ export const addDataUrl = async (f: Partial): Promise => { - const imageMetadata = await getImageMetadata(f.url) as ImageFileMetadata; - return { - name: f.name, - type: f.type, - size: f.size, - lastModified: f.lastModified, - resolvedType: ContentType.IMAGE, - url: f.url, - metadata: imageMetadata, - }; +interface FileWithResolvedContentType extends Partial { + resolvedType: ContentType; +} + +export interface TextFile extends FileWithResolvedContentType { + resolvedType: ContentType.TEXT; + metadata?: TextMetadata; +} + +export const augmentTextFile = async >(file: T): Promise => { + const contents = typeof file?.text === 'function' ? await file.text() : ''; + const fileMutable = file as unknown as Record; + fileMutable.metadata = await getTextMetadata(contents ?? '', file.name); + return fileMutable as unknown as TextFile; }; -export interface AudioFileMetadata { - previewUrl?: string; - duration?: number; +export interface ImageFile extends FileWithResolvedContentType { + resolvedType: ContentType.IMAGE; + metadata?: ImageMetadata; } -export interface AudioFile extends FileWithResolvedType { - metadata?: AudioFileMetadata; +export const augmentImageFile = async >(file: T): Promise => { + const fileMutable = file as unknown as Record; + fileMutable.metadata = await getImageMetadata(file.url); + return fileMutable as unknown as ImageFile; +}; + +export interface AudioFile extends FileWithResolvedContentType { + resolvedType: ContentType.AUDIO; + metadata?: AudioMetadata; } -export const augmentAudioFile = async (f: File): Promise => { - const previewUrl = await readAsDataURL(f); - const audioExtensions = await getAudioMetadata(previewUrl, f.type) as AudioFileMetadata; - return { - name: f.name, - type: f.type, - size: f.size, - lastModified: f.lastModified, - resolvedType: ContentType.AUDIO, - originalFile: f, - metadata: { - previewUrl, - duration: audioExtensions.duration, - }, - }; +export const augmentAudioFile = async >(file: T): Promise => { + const fileMutable = file as unknown as Record; + fileMutable.metadata = await getAudioMetadata(file.url, file.type); + return fileMutable as unknown as AudioFile; }; export interface BinaryFileMetadata { contents: ArrayBuffer; } -export interface BinaryFile extends FileWithResolvedType { +export interface BinaryFile extends FileWithResolvedContentType { + resolvedType: ContentType.BINARY; metadata?: BinaryFileMetadata; } -const augmentBinaryFile = async (f: File): Promise => { - const arrayBuffer = await readAsArrayBuffer(f); - return { - name: f.name, - type: f.type, - size: f.size, - lastModified: f.lastModified, - resolvedType: ContentType.BINARY, - originalFile: f, - metadata: { - contents: arrayBuffer, - }, +export const augmentBinaryFile = async >(file: T): Promise => { + const metadata = {} as BinaryFileMetadata; + if (typeof file?.arrayBuffer === 'function') { + metadata.contents = await file.arrayBuffer(); } + const fileMutable = file as unknown as Record; + fileMutable.metadata = metadata; + return fileMutable as unknown as BinaryFile; }; -export interface VideoFileMetadata { - previewUrl?: string; - width?: number; - height?: number; -} - -export interface VideoFile extends FileWithResolvedType { +export interface VideoFile extends FileWithResolvedContentType { + resolvedType: ContentType.VIDEO; metadata?: VideoFileMetadata; } -export const augmentVideoFile = async (f: File): Promise => { - const previewUrl = await readAsDataURL(f); - const videoMetadata = await getVideoMetadata(previewUrl); - return { - name: f.name, - type: f.type, - size: f.size, - lastModified: f.lastModified, - resolvedType: ContentType.VIDEO, - originalFile: f, - metadata: { - previewUrl, - width: videoMetadata.width as number, - height: videoMetadata.height as number, - }, - }; +export const augmentVideoFile = async >(file: T): Promise => { + const fileMutable = file as unknown as Record; + fileMutable.metadata = await getVideoMetadata(file.url); + return fileMutable as unknown as VideoFile; }; export type AugmentedFile = TextFile | ImageFile | AudioFile | VideoFile | BinaryFile; diff --git a/packages/web-kitchensink-reactnext/src/utils/numeral.ts b/packages/web-kitchensink-reactnext/src/utils/numeral.ts index dcb2b86..57bbe43 100644 --- a/packages/web-kitchensink-reactnext/src/utils/numeral.ts +++ b/packages/web-kitchensink-reactnext/src/utils/numeral.ts @@ -20,7 +20,7 @@ export const formatFileSize = (size?: number) => { } if (size < (2 ** 10)) { - return `${formatNumeral(size)} bytes`; + return `${formatNumeral(size)} byte(s)`; } if (size < (2 ** 20)) { diff --git a/packages/web-kitchensink-reactnext/tsconfig.json b/packages/web-kitchensink-reactnext/tsconfig.json index 0824439..e214126 100644 --- a/packages/web-kitchensink-reactnext/tsconfig.json +++ b/packages/web-kitchensink-reactnext/tsconfig.json @@ -27,6 +27,7 @@ "@tesseract-design/web-option-react": ["./src/categories/option/react"], "@tesseract-design/web-number-react": ["./src/categories/number/react"], "@tesseract-design/web-navigation-react": ["./src/categories/navigation/react"], + "@modal-soft/*": ["./src/packages/*"], } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],