Browse Source

Improve implementation of binary and file previews

Add line numbers and highlighting for binary and text file previews.
pull/1/head
TheoryOfNekomata 1 year ago
parent
commit
605f6097e6
25 changed files with 1266 additions and 574 deletions
  1. BIN
      packages/web-kitchensink-reactnext/public/binary.bin
  2. +106
    -0
      packages/web-kitchensink-reactnext/public/code.py
  3. +95
    -0
      packages/web-kitchensink-reactnext/public/plaintext.txt
  4. +143
    -130
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx
  5. +146
    -83
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/BinaryFilePreview/index.tsx
  6. +68
    -59
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/ImageFilePreview/index.tsx
  7. +139
    -99
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/TextFilePreview/index.tsx
  8. +57
    -46
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx
  9. +4
    -4
      packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/blob/metadata.ts
  10. +2
    -2
      packages/web-kitchensink-reactnext/src/categories/blob/react/index.ts
  11. +4
    -4
      packages/web-kitchensink-reactnext/src/categories/information/react/components/KeyValueTable/index.tsx
  12. +11
    -1
      packages/web-kitchensink-reactnext/src/packages/audio-utils/index.ts
  13. +10
    -1
      packages/web-kitchensink-reactnext/src/packages/image-utils/index.ts
  14. +117
    -0
      packages/web-kitchensink-reactnext/src/packages/react-binary-data-canvas/index.tsx
  15. +134
    -0
      packages/web-kitchensink-reactnext/src/packages/react-prism/index.tsx
  16. +2
    -1
      packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/WaveformCanvas/index.tsx
  17. +34
    -8
      packages/web-kitchensink-reactnext/src/packages/text-utils/index.ts
  18. +14
    -1
      packages/web-kitchensink-reactnext/src/packages/video-utils/index.ts
  19. +96
    -3
      packages/web-kitchensink-reactnext/src/pages/categories/blob/index.tsx
  20. +1
    -1
      packages/web-kitchensink-reactnext/src/pages/index.tsx
  21. +31
    -0
      packages/web-kitchensink-reactnext/src/styles/globals.css
  22. +1
    -1
      packages/web-kitchensink-reactnext/src/styles/theme.ts
  23. +49
    -129
      packages/web-kitchensink-reactnext/src/utils/blob.ts
  24. +1
    -1
      packages/web-kitchensink-reactnext/src/utils/numeral.ts
  25. +1
    -0
      packages/web-kitchensink-reactnext/tsconfig.json

BIN
packages/web-kitchensink-reactnext/public/binary.bin View File


+ 106
- 0
packages/web-kitchensink-reactnext/public/code.py View File

@@ -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)

+ 95
- 0
packages/web-kitchensink-reactnext/public/plaintext.txt View File

@@ -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.

+ 143
- 130
packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx View File

@@ -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<React.HTMLProps<AudioFilePreviewDerivedComponent>, 'controls'> {
file?: File;
export interface AudioFilePreviewProps<F extends Partial<File> = Partial<File>> extends Omit<React.HTMLProps<AudioFilePreviewDerivedComponent>, 'controls'> {
file?: F;
disabled?: boolean;
enhanced?: boolean;
}
@@ -27,12 +27,15 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
file,
style,
className,
enhanced = false,
enhanced: enhancedProp = false,
disabled = false,
...etcProps
}, forwardedRef) => {
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<AudioFilePreviewDerivedComponen
});
const formId = React.useId();

if (!augmentedFile) {
const [enhanced, setEnhanced] = React.useState(false);
React.useEffect(() => {
setEnhanced(enhancedProp);
}, [enhancedProp]);

if (!fileWithMetadata) {
return null;
}

@@ -79,7 +87,7 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
>
<div className="h-full relative col-span-2">
{
typeof augmentedFile.metadata?.previewUrl === 'string'
typeof fileWithMetadata.url === 'string'
&& (
<div
className="w-full h-full bg-black flex flex-col items-stretch"
@@ -96,96 +104,101 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
onTimeUpdate={updateSeekFromPlayback}
>
<source
src={augmentedFile.metadata.previewUrl}
type={augmentedFile.type}
src={fileWithMetadata.url}
type={fileWithMetadata.type}
/>
Audio playback not supported.
</audio>
<WaveformCanvas
className={clsx(
'sm:absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-primary/10',
visualizationMode !== 'waveform' && 'opacity-0',
)}
ref={mediaControllerRef}
data-testid="preview"
barWidth={1}
barGap={1}
progressColor={`rgb(${theme.primary})`}
waveColor={`rgb(${theme.primary.split(' ').map((c) => Math.floor(Number(c) / 2)).join(' ')})`}
interact
// waveColor={`rgb(${theme.primary})`}
// barHeight={4}
// minPxPerSec={20000}
// hideScrollbar
// autoCenter
// autoScroll
/>
<SpectrogramCanvas
className={clsx(
'sm:absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-primary/10 pointer-events-none',
visualizationMode !== 'spectrum' && 'opacity-0',
)}
ref={mediaControllerRef}
data-testid="preview"
barWidth={1}
barGap={1}
waveColor={`rgb(${theme.primary})`}
cursorWidth={2}
minPxPerSec={20000}
hideScrollbar
autoCenter
autoScroll
/>
<div className="flex gap-4 absolute top-0 right-0 z-[5] px-4">
<label
className={clsx(
'h-12 flex items-center justify-center leading-none gap-4 select-none',
)}
>
<input
type="radio"
name="visualizationMode"
value="waveform"
className="sr-only peer/waveform"
onChange={handleVisualizationModeChange}
defaultChecked
/>
<span
{enhanced && (
<>
<WaveformCanvas
className={clsx(
'block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded text-primary',
'peer-focus/waveform:text-secondary',
'peer-active/waveform:text-tertiary',
'peer-checked/waveform:text-tertiary',
'peer-disabled/waveform:text-primary peer-disabled/waveform:cursor-not-allowed peer-disabled/waveform:opacity-50',
'sm:absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-primary/10',
visualizationMode !== 'waveform' && 'opacity-0',
)}
>
Waveform
</span>
</label>
<label
className={clsx(
'h-12 flex items-center justify-center leading-none gap-4 select-none',
)}
>
<input
type="radio"
name="visualizationMode"
value="spectrum"
className="sr-only peer/waveform"
onChange={handleVisualizationModeChange}
ref={mediaControllerRef}
data-testid="preview"
barWidth={1}
barGap={1}
progressColor={`rgb(${theme.primary})`}
waveColor={`rgb(${theme.primary.split(' ').map((c) => Math.floor(Number(c) / 2)).join(' ')})`}
interact
// waveColor={`rgb(${theme.primary})`}
// barHeight={4}
// minPxPerSec={20000}
// hideScrollbar
// autoCenter
// autoScroll
/>
<span
<SpectrogramCanvas
className={clsx(
'block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded text-primary',
'peer-focus/waveform:text-secondary',
'peer-active/waveform:text-tertiary',
'peer-checked/waveform:text-tertiary',
'peer-disabled/waveform:text-primary peer-disabled/waveform:cursor-not-allowed peer-disabled/waveform:opacity-50',
'sm:absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-primary/10 pointer-events-none',
visualizationMode !== 'spectrum' && 'opacity-0',
)}
>
Spectrum
</span>
</label>
</div>
ref={mediaControllerRef}
data-testid="preview"
barWidth={1}
barGap={1}
waveColor={`rgb(${theme.primary})`}
cursorWidth={2}
minPxPerSec={20000}
hideScrollbar
autoCenter
autoScroll
/>
<div className="flex gap-4 absolute top-0 right-0 z-[5] px-4">
<label
className={clsx(
'h-12 flex items-center justify-center leading-none gap-4 select-none',
)}
>
<input
type="radio"
name="visualizationMode"
value="waveform"
className="sr-only peer/waveform"
onChange={handleVisualizationModeChange}
defaultChecked
/>
<span
className={clsx(
'block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded text-primary',
'peer-focus/waveform:text-secondary',
'peer-active/waveform:text-tertiary',
'peer-checked/waveform:text-tertiary',
'peer-disabled/waveform:text-primary peer-disabled/waveform:cursor-not-allowed peer-disabled/waveform:opacity-50',
)}
>
Waveform
</span>
</label>
<label
className={clsx(
'h-12 flex items-center justify-center leading-none gap-4 select-none',
)}
>
<input
type="radio"
name="visualizationMode"
value="spectrum"
className="sr-only peer/waveform"
onChange={handleVisualizationModeChange}
/>
<span
className={clsx(
'block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded text-primary',
'peer-focus/waveform:text-secondary',
'peer-active/waveform:text-tertiary',
'peer-checked/waveform:text-tertiary',
'peer-disabled/waveform:text-primary peer-disabled/waveform:cursor-not-allowed peer-disabled/waveform:opacity-50',
)}
>
Spectrum
</span>
</label>
</div>
</>
)}
</div>
{enhanced && (
<div className="w-full flex-shrink-0 h-10 flex gap-4 items-center">
@@ -298,83 +311,83 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
)
}
</div>
<div
className="flex-shrink-0 m-0 flex flex-col gap-4 justify-between"
>
<div className="flex-shrink-0 m-0 flex flex-col gap-4 justify-between">
<KeyValueTable
hiddenKeys
data-testid="infoBox"
properties={[
{
Boolean(fileWithMetadata.name) && {
key: 'Name',
className: 'font-bold',
valueProps: {
ref: filenameRef,
children: augmentedFile.name,
children: fileWithMetadata.name,
},
},
{
(enhanced && Boolean(getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) || '(Loading)') || Boolean(fileWithMetadata.type)) && {
key: 'Type',
valueProps: {
className: clsx(
!getMimeTypeDescription(augmentedFile.type, augmentedFile.name) && 'opacity-50'
!getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) && 'opacity-50'
),
children: getMimeTypeDescription(augmentedFile.type, augmentedFile.name) || '(Loading)',
children: getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) || '(Loading)',
},
},
{
(enhanced && Boolean(formatFileSize(fileWithMetadata.size) || '(Loading)') || Boolean(fileWithMetadata.size)) && {
key: 'Size',
valueProps: {
className: clsx(
!formatFileSize(augmentedFile.size) && 'opacity-50'
!formatFileSize(fileWithMetadata.size) && 'opacity-50'
),
title: `${formatNumeral(augmentedFile.size ?? 0)} bytes`,
children: formatFileSize(augmentedFile.size) || '(Loading)',
title: `${formatNumeral(fileWithMetadata.size ?? 0)} byte(s)`,
children: formatFileSize(fileWithMetadata.size) || '(Loading)',
},
},
typeof augmentedFile.metadata?.duration === 'number'
typeof fileWithMetadata.metadata?.duration === 'number'
&& {
key: 'Duration',
valueProps: {
className: clsx(
!formatSecondsDurationPrecise(augmentedFile.metadata.duration) && 'opacity-50'
!formatSecondsDurationPrecise(fileWithMetadata.metadata.duration) && 'opacity-50'
),
title: `${formatNumeral(augmentedFile.metadata.duration ?? 0)} seconds`,
children: formatSecondsDurationPrecise(augmentedFile.metadata.duration),
title: `${formatNumeral(fileWithMetadata.metadata.duration ?? 0)} second(s)`,
children: formatSecondsDurationPrecise(fileWithMetadata.metadata.duration),
},
},
]}
/>
<form
id={formId}
onSubmit={handleAction}
className="flex gap-4"
>
<fieldset
disabled={disabled || typeof error !== 'undefined'}
className="contents"
{enhanced && (
<form
id={formId}
onSubmit={handleAction}
className="flex gap-4"
>
<legend className="sr-only">
Controls
</legend>
<button
type="submit"
name="action"
value="download"
className={clsx(
'h-12 flex bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
)}
<fieldset
disabled={disabled || typeof error !== 'undefined'}
className="contents"
>
<legend className="sr-only">
Controls
</legend>
<button
type="submit"
name="action"
value="download"
className={clsx(
'h-12 flex bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
)}
>
<span
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
>
Download
</span>
</button>
</fieldset>
</form>
</button>
</fieldset>
</form>
)}
</div>
</div>
);


+ 146
- 83
packages/web-kitchensink-reactnext/src/categories/blob/react/components/BinaryFilePreview/index.tsx View File

@@ -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<F extends Partial<File> = Partial<File>> extends React.HTMLProps<BinaryFilePreviewDerivedComponent> {
file?: F;
enhanced?: boolean;
}

export const BinaryFilePreview: React.FC<BinaryFilePreviewProps> = ({
file: f,
}) => (
<div className="flex gap-4 w-full h-full">
<div className={`h-full w-1/3 flex-shrink-0`}>
{
f.metadata && (f.metadata?.contents instanceof ArrayBuffer)
&& (
<div
data-testid="preview"
role="presentation"
className="w-full h-full select-none overflow-hidden text-xs"
>
<pre className="overflow-visible">
<code>
{
(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<BinaryFilePreviewDerivedComponent, BinaryFilePreviewProps>(({
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')
}
</code>
</pre>
</div>
)
}
</div>
<dl className="w-2/3 flex-shrink-0 m-0" data-testid="infoBox">
<div className="w-full">
<dt className="sr-only">
Name
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
title={f.name}
>
{f.name}
</dd>
</div>
<div className="w-full">
<dt className="sr-only">
Type
</dt>
<dd
title={f.type}
className="m-0 w-full text-ellipsis overflow-hidden"
>
{getMimeTypeDescription(f.type, f.name)}
</dd>
if (!fileWithMetadata) {
return null;
}

return (
<div
className={clsx(
'flex flex-col sm:grid sm:grid-cols-3 gap-8 w-full',
className,
)}
style={style}
>
<div className="h-full relative">
<div className="absolute top-0 left-0 w-full h-full">
{
fileWithMetadata.metadata && (fileWithMetadata.metadata?.contents instanceof ArrayBuffer)
&& (
<div
{...etcProps}
data-testid="preview"
role="presentation"
className="w-full h-full select-none overflow-hidden text-xs"
ref={forwardedRef}
>
<BinaryDataCanvas
arrayBuffer={fileWithMetadata.metadata?.contents}
className="overflow-visible binary-file-preview"
headers
byteClassName={(byte, index) => {
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';
}}
/>
</div>
)
}
</div>
</div>
<div className="w-full">
<dt className="sr-only">
Size
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
title={`${formatNumeral(f.size ?? 0)} bytes`}
>
{formatFileSize(f.size)}
</dd>
<div className="col-span-2 flex-shrink-0 m-0 flex flex-col gap-4 justify-between">
<KeyValueTable
hiddenKeys
data-testid="infoBox"
properties={[
Boolean(fileWithMetadata.name) && {
key: 'Name',
className: 'font-bold',
valueProps: {
children: fileWithMetadata.name,
title: fileWithMetadata.name,
},
},
(enhanced && Boolean(getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) || '(Loading)') || Boolean(fileWithMetadata.type)) && {
key: 'Type',
valueProps: {
className: clsx(
!getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) && 'opacity-50'
),
children: getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) || '(Loading)',
},
},
(enhanced && Boolean(formatFileSize(fileWithMetadata.size) || '(Loading)') || Boolean(fileWithMetadata.size)) && {
key: 'Size',
valueProps: {
className: clsx(
!formatFileSize(fileWithMetadata.size) && 'opacity-50'
),
title: `${formatNumeral(fileWithMetadata.size ?? 0)} byte(s)`,
children: formatFileSize(fileWithMetadata.size) || '(Loading)',
},
},
]}
/>
</div>
</dl>
</div>
);
</div>
);
});

BinaryFilePreview.displayName = 'BinaryFilePreview';

+ 68
- 59
packages/web-kitchensink-reactnext/src/categories/blob/react/components/ImageFilePreview/index.tsx View File

@@ -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<React.HTMLProps<ImageFilePreviewDerivedComponent>, 'src' | 'alt'> {
file?: Partial<FileWithDataUrl>;
export interface ImageFilePreviewProps<F extends Partial<File> = Partial<File>> extends Omit<React.HTMLProps<ImageFilePreviewDerivedComponent>, 'src' | 'alt'> {
file?: F;
disabled?: boolean;
enhanced?: boolean;
}

export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponent, ImageFilePreviewProps>(({
@@ -17,10 +18,11 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
className,
style,
disabled = false,
enhanced: enhancedProp = false,
...etcProps
}, forwardedRef) => {
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<ImageFilePreviewDerivedComponen
forwardedRef,
});

if (!(augmentedFile)) {
const [enhanced, setEnhanced] = React.useState(false);
React.useEffect(() => {
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<ImageFilePreviewDerivedComponen
>
<div className="h-full relative">
<div className="sm:absolute top-0 left-0 w-full sm:h-full z-[3]">
{typeof augmentedFile.url === 'string' && (
{typeof fileWithMetadata.url === 'string' && (
<img
{...etcProps}
ref={imageRef}
@@ -63,7 +70,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
'object-cover w-full': !fullScreen,
},
)}
src={augmentedFile.url}
src={fileWithMetadata.url}
alt=""
data-testid="preview"
/>
@@ -82,47 +89,47 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
hiddenKeys
data-testid="infoBox"
properties={[
{
Boolean(fileWithMetadata.name) && {
key: 'Name',
className: 'font-bold',
valueProps: {
ref: filenameRef,
children: augmentedFile.name,
children: fileWithMetadata.name,
},
},
{
(enhanced && Boolean(getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) || '(Loading)') || Boolean(fileWithMetadata.type)) && {
key: 'Type',
valueProps: {
className: clsx(
!getMimeTypeDescription(augmentedFile.type, augmentedFile.name) && 'opacity-50'
!getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) && 'opacity-50'
),
children: getMimeTypeDescription(augmentedFile.type, augmentedFile.name) || '(Loading)',
children: getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) || '(Loading)',
},
},
{
(enhanced && Boolean(formatFileSize(fileWithMetadata.size) || '(Loading)') || Boolean(fileWithMetadata.size)) && {
key: 'Size',
valueProps: {
className: clsx(
!formatFileSize(augmentedFile.size) && 'opacity-50'
!formatFileSize(fileWithMetadata.size) && 'opacity-50'
),
title: `${formatNumeral(augmentedFile.size ?? 0)} bytes`,
children: formatFileSize(augmentedFile.size) || '(Loading)',
title: `${formatNumeral(fileWithMetadata.size ?? 0)} byte(s)`,
children: formatFileSize(fileWithMetadata.size) || '(Loading)',
},
},
typeof augmentedFile.metadata?.width === 'number'
&& typeof augmentedFile.metadata?.height === 'number'
typeof fileWithMetadata.metadata?.width === 'number'
&& typeof fileWithMetadata.metadata?.height === 'number'
&& {
key: 'Pixel Dimensions',
valueProps: {
children: `${formatNumeral(augmentedFile.metadata.width)} × ${formatNumeral(augmentedFile.metadata.height)} pixels`,
children: `${formatNumeral(fileWithMetadata.metadata.width)} × ${formatNumeral(fileWithMetadata.metadata.height)} pixel(s)`,
},
},
Array.isArray(augmentedFile.metadata?.palette)
Array.isArray(fileWithMetadata.metadata?.palette)
&& {
key: 'Palette',
valueProps: {
className: 'mt-1',
children: augmentedFile.metadata?.palette.map((rgb, i) => (
children: fileWithMetadata.metadata?.palette.map((rgb, i) => (
<React.Fragment
key={rgb.join(' ')}
>
@@ -166,55 +173,57 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
},
]}
/>
<form
onSubmit={handleAction}
className="flex gap-4"
>
<fieldset
disabled={disabled || cannotDisplayPicture}
className="contents"
{enhanced && (
<form
onSubmit={handleAction}
className="flex gap-4"
>
<legend className="sr-only">
Controls
</legend>
<button
type="submit"
name="action"
value="toggleFullScreen"
className={clsx(
'h-12 flex bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'fixed top-0 left-0 w-full h-full opacity-0 z-[3]': fullScreen,
}
)}
<fieldset
disabled={disabled || cannotDisplayPicture}
className="contents"
>
<legend className="sr-only">
Controls
</legend>
<button
type="submit"
name="action"
value="toggleFullScreen"
className={clsx(
'h-12 flex bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'fixed top-0 left-0 w-full h-full opacity-0 z-[3]': fullScreen,
}
)}
>
<span
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
>
Preview
</span>
</button>
{' '}
<button
type="submit"
name="action"
value="download"
className={clsx(
'h-12 flex bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
)}
>
</button>
{' '}
<button
type="submit"
name="action"
value="download"
className={clsx(
'h-12 flex bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
)}
>
<span
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
>
Download
</span>
</button>
</fieldset>
</form>
</button>
</fieldset>
</form>
)}
</div>
</div>
);


+ 139
- 99
packages/web-kitchensink-reactnext/src/categories/blob/react/components/TextFilePreview/index.tsx View File

@@ -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<F extends Partial<File> = Partial<File>> extends React.HTMLProps<TextFilePreviewDerivedComponent> {
file?: F;
enhanced?: boolean;
}

export const TextFilePreview: React.FC<TextFilePreviewProps> = ({
file: f,
}) => (
<div className="flex gap-4 w-full h-full">
<div className={`h-full w-1/3 flex-shrink-0`}>
{
typeof f.metadata?.contents === 'string'
&& (
<div
data-testid="preview"
role="presentation"
className="w-full h-full select-none overflow-hidden text-xs"
>
<pre className="overflow-visible">
{
typeof f.metadata.scheme === 'string'
&& (
<code
dangerouslySetInnerHTML={{
__html: Prism.highlight(
f.metadata.contents,
Prism.languages[f.metadata.scheme],
f.metadata.scheme,
).split('\n').slice(0, 15).join('\n'),
}}
style={{
tabSize: 2,
}}
/>
)
}
{
typeof f.metadata.scheme !== 'string'
&& (
<code>
{f.metadata.contents}
</code>
)
}
</pre>
</div>
)
}
</div>
<dl className="w-2/3 flex-shrink-0 m-0" data-testid="infoBox">
<div className="w-full">
<dt className="sr-only">
Name
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
title={f.name}
>
{f.name}
</dd>
</div>
<div className="w-full">
<dt className="sr-only">
Type
</dt>
<dd
title={f.type}
className="m-0 w-full text-ellipsis overflow-hidden"
>
{typeof f.metadata?.schemeTitle === 'string' ? `${f.metadata.schemeTitle} Source` : 'Text File'}
</dd>
export const TextFilePreview = React.forwardRef<TextFilePreviewDerivedComponent, TextFilePreviewProps>(({
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 (
<div
className={clsx(
'flex flex-col sm:grid sm:grid-cols-3 gap-8 w-full',
className,
)}
style={style}
>
<div className="h-full relative col-span-2">
<div className="absolute top-0 left-0 w-full h-full">
{
typeof fileWithMetadata.metadata?.contents === 'string'
&& (
<div
{...etcProps}
data-testid="preview"
role="presentation"
className="w-full h-full select-none overflow-hidden text-xs"
ref={forwardedRef}
>
<Prism
code={fileWithMetadata.metadata.contents}
language={fileWithMetadata.metadata.scheme}
lineNumbers={Boolean(fileWithMetadata.metadata.scheme)}
maxLineNumber={20}
/>
</div>
)
}
</div>
</div>
<div className="w-full">
<dt className="sr-only">
Size
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
title={`${formatNumeral(f.size ?? 0)} bytes`}
>
{formatFileSize(f.size)}
</dd>
<div className="flex-shrink-0 m-0 flex flex-col gap-4 justify-between">
<KeyValueTable
hiddenKeys
data-testid="infoBox"
properties={[
Boolean(fileWithMetadata.name) && {
key: 'Name',
className: 'font-bold',
valueProps: {
children: fileWithMetadata.name,
title: fileWithMetadata.name,
},
},
(enhanced && Boolean(getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) || '(Loading)') || Boolean(fileWithMetadata.type)) && {
key: 'Type',
valueProps: {
children: typeof fileWithMetadata.metadata?.schemeTitle === 'string' ? `${fileWithMetadata.metadata.schemeTitle} Source` : 'Text File',
},
},
(enhanced && Boolean(formatFileSize(fileWithMetadata.size) || '(Loading)') || Boolean(fileWithMetadata.size)) && {
key: 'Size',
valueProps: {
className: clsx(
!formatFileSize(fileWithMetadata.size) && 'opacity-50'
),
title: `${formatNumeral(fileWithMetadata.size ?? 0)} byte(s)`,
children: formatFileSize(fileWithMetadata.size) || '(Loading)',
},
},
Array.isArray(fileWithMetadata.metadata?.languageMatches)
&& {
key: 'Language',
valueProps: {
children: (
<dl
className="text-sm"
>
{fileWithMetadata.metadata!.languageMatches.slice(0, 3).map(([language, probability], index) => {
return (
<div key={index} className="flex justify-between">
<dt>
{language.slice(0, 1).toUpperCase()}
{language.slice(1)}
</dt>
<dd className="tabular-nums">
({(probability * 100).toFixed(3)}%)
</dd>
</div>
);
})}
</dl>
),
},
},
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`,
},
},
]}
/>
</div>
{
typeof f.metadata?.language === 'string'
&& (
<div>
<dt className="sr-only">
Language
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
>
{f.metadata.language.slice(0, 1).toUpperCase()}
{f.metadata.language.slice(1)}
</dd>
</div>
)
}
</dl>
</div>
);
</div>
);
});

TextFilePreview.displayName = 'TextFilePreview';

+ 57
- 46
packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx View File

@@ -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<React.HTMLProps<VideoFilePreviewDerivedComponent>, 'controls'> {
file?: File;
export interface VideoFilePreviewProps<F extends Partial<File> = Partial<File>> extends Omit<React.HTMLProps<VideoFilePreviewDerivedComponent>, 'controls'> {
file?: F;
disabled?: boolean;
enhanced?: boolean;
}
@@ -19,11 +19,12 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
className,
style,
disabled = false,
enhanced = false,
enhanced: enhancedProp = false,
...etcProps
}, forwardedRef) => {
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<VideoFilePreviewDerivedComponen
});
const formId = React.useId();

if (!augmentedFile) {
const [enhanced, setEnhanced] = React.useState(false);
React.useEffect(() => {
setEnhanced(enhancedProp);
}, [enhancedProp]);

if (!fileWithMetadata) {
return null;
}

@@ -67,7 +73,7 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
>
<div className="h-full relative col-span-2">
{
typeof augmentedFile.metadata?.previewUrl === 'string'
typeof fileWithMetadata.url === 'string'
&& (
<div
className="w-full h-full bg-black flex flex-col items-stretch"
@@ -86,8 +92,8 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
controls={!enhanced}
>
<source
src={augmentedFile.metadata.previewUrl}
type={augmentedFile.type}
src={fileWithMetadata.url}
type={fileWithMetadata.type}
/>
</video>
<button
@@ -221,73 +227,78 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
hiddenKeys
data-testid="infoBox"
properties={[
{
Boolean(fileWithMetadata.name) && {
key: 'Name',
className: 'font-bold',
valueProps: {
ref: filenameRef,
children: augmentedFile.name,
children: fileWithMetadata.name,
},
},
{
(enhanced && Boolean(getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) || '(Loading)') || Boolean(fileWithMetadata.type)) && {
key: 'Type',
valueProps: {
className: clsx(
!getMimeTypeDescription(augmentedFile.type, augmentedFile.name) && 'opacity-50'
!getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) && 'opacity-50'
),
children: getMimeTypeDescription(augmentedFile.type, augmentedFile.name) || '(Loading)',
children: getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) || '(Loading)',
},
},
{
(enhanced && Boolean(formatFileSize(fileWithMetadata.size) || '(Loading)') || Boolean(fileWithMetadata.size)) && {
key: 'Size',
valueProps: {
className: clsx(
!formatFileSize(augmentedFile.size) && 'opacity-50'
!formatFileSize(fileWithMetadata.size) && 'opacity-50'
),
title: `${formatNumeral(augmentedFile.size ?? 0)} bytes`,
children: formatFileSize(augmentedFile.size) || '(Loading)',
title: `${formatNumeral(fileWithMetadata.size ?? 0)} byte(s)`,
children: formatFileSize(fileWithMetadata.size) || '(Loading)',
},
},
typeof augmentedFile.metadata?.width === 'number'
&& typeof augmentedFile.metadata?.height === 'number'
typeof fileWithMetadata.metadata?.width === 'number'
&& typeof fileWithMetadata.metadata?.height === 'number'
&& {
key: 'Pixel Dimensions',
valueProps: {
children: `${formatNumeral(augmentedFile.metadata.width)} × ${formatNumeral(augmentedFile.metadata.height)} pixels`,
children: `${formatNumeral(fileWithMetadata.metadata.width)} × ${formatNumeral(fileWithMetadata.metadata.height)} pixel(s)`,
},
},
]}
/>
<form
id={formId}
onSubmit={handleAction}
className="flex gap-4"
>
<fieldset
disabled={disabled || typeof error !== 'undefined'}
className="contents"
>
<legend className="sr-only">
Controls
</legend>
<button
type="submit"
name="action"
value="download"
className={clsx(
'h-12 flex bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
)}
{
enhanced
&& (
<form
id={formId}
onSubmit={handleAction}
className="flex gap-4"
>
<fieldset
disabled={disabled || typeof error !== 'undefined'}
className="contents"
>
<legend className="sr-only">
Controls
</legend>
<button
type="submit"
name="action"
value="download"
className={clsx(
'h-12 flex bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
)}
>
<span
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
>
Download
</span>
</button>
</fieldset>
</form>
</button>
</fieldset>
</form>
)
}
</div>
</div>
);


+ 4
- 4
packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/blob/metadata.ts View File

@@ -35,9 +35,9 @@ export const useFileUrl = (options: UseFileUrlOptions) => {
}), [fileWithUrl, loading]);
};

export interface UseFileMetadataOptions<T extends Partial<File> = Partial<File>> {
file?: File;
augmentFunction: (file: File) => Promise<T>;
export interface UseFileMetadataOptions<T extends Partial<File> = Partial<File>, U extends Partial<File> = Partial<File>> {
file?: U;
augmentFunction: (file: U) => Promise<T>;
}

export const useFileMetadata = <T extends Partial<File>>(options: UseFileMetadataOptions<T>) => {
@@ -67,7 +67,7 @@ export const useFileMetadata = <T extends Partial<File>>(options: UseFileMetadat
}, [file, augmentFunction]);

return React.useMemo(() => ({
augmentedFile: fileWithMetadata,
fileWithMetadata,
error,
loading,
}), [fileWithMetadata, loading, error]);


+ 2
- 2
packages/web-kitchensink-reactnext/src/categories/blob/react/index.ts View File

@@ -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';


+ 4
- 4
packages/web-kitchensink-reactnext/src/categories/information/react/components/KeyValueTable/index.tsx View File

@@ -22,7 +22,7 @@ export const KeyValueTable = React.forwardRef<KeyValueTableDerivedElement, KeyVa
<dl
{...etcProps}
className={clsx(
'flex flex-wrap gap-y-1'
'grid gap-y-1 grid-cols-3',
)}
ref={forwardedRef}
>
@@ -33,7 +33,7 @@ export const KeyValueTable = React.forwardRef<KeyValueTableDerivedElement, KeyVa
className={clsx('contents', property.className)}
>
<dt
className={clsx(hiddenKeys && 'sr-only', 'w-1/3 pr-4')}
className={clsx(hiddenKeys && 'sr-only', 'pr-4')}
>
{property.key}
</dt>
@@ -41,8 +41,8 @@ export const KeyValueTable = React.forwardRef<KeyValueTableDerivedElement, KeyVa
{...(property.valueProps ?? {})}
className={clsx(
'm-0 text-ellipsis overflow-hidden',
!hiddenKeys && 'w-2/3',
hiddenKeys && 'w-full',
!hiddenKeys && 'col-span-2',
hiddenKeys && 'col-span-3',
property.valueProps?.className,
)}
>


packages/web-kitchensink-reactnext/src/utils/audio.ts → packages/web-kitchensink-reactnext/src/packages/audio-utils/index.ts View File

@@ -1,6 +1,11 @@
import WaveSurfer from 'wavesurfer.js';

export const getAudioMetadata = (audioUrl: string, fileType: string) => new Promise<Record<string, string | number>>(async (resolve, reject) => {
export interface AudioMetadata {
duration?: number;
}

export const getAudioMetadata = (audioUrl?: string, fileType?: string) => new Promise<AudioMetadata>(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);

packages/web-kitchensink-reactnext/src/utils/image.ts → packages/web-kitchensink-reactnext/src/packages/image-utils/index.ts View File

@@ -1,6 +1,15 @@
import ColorThief from 'colorthief';

export const getImageMetadata = (imageUrl?: string) => new Promise<Record<string, string | number | [number, number, number][]>>((resolve, reject) => {
type RgbTuple = [number, number, number];

export interface ImageMetadata {
width?: number;
height?: number;
palette?: RgbTuple[];
}

export const getImageMetadata = (imageUrl?: string) => new Promise<ImageMetadata>((resolve, reject) => {
// TODO server side
const image = new Image();

image.addEventListener('load', async (imageLoadEvent) => {

+ 117
- 0
packages/web-kitchensink-reactnext/src/packages/react-binary-data-canvas/index.tsx View File

@@ -0,0 +1,117 @@
import * as React from 'react';
import clsx from 'clsx';

type BinaryDataCanvasDerivedElement = HTMLPreElement;

export interface BinaryDataCanvasProps extends Omit<React.HTMLProps<BinaryDataCanvasDerivedElement>, '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<BinaryDataCanvasDerivedElement, BinaryDataCanvasProps>(({
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 (
<div className={clsx(
'flex gap-2 leading-tight',
className,
)}>
{headers && (
<pre
style={{
padding: 0,
overflow: 'visible',
margin: 0,
textAlign: 'right',
}}
>
<code>
{'\u00a0'}
</code>
{bytesGrouped.map((_, i) => (
<React.Fragment key={i}>
{'\n'}
<code
style={{
whiteSpace: 'nowrap',
}}
>
0x{(BYTES_PER_LINE * i).toString(16).padStart(2, '0')}
</code>
</React.Fragment>
))}
</pre>
)}
<div>
<pre
{...etcProps}
ref={forwardedRef}
className="leading-tight"
>
<code>
{headers && new Array(BYTES_PER_LINE).fill(0).map((_, i) => (
<span
key={i}
>
{i > 0 && ' '}
{i.toString(16).padStart(2, '+')}
</span>
))}
{
bytesGrouped.map((line: number[], l) => (
<React.Fragment key={l}>
{l > 0 && '\n'}
<span
className={lineClassName}
>
{line.map((byte, b) => (
<React.Fragment key={b}>
{b > 0 && ' '}
<span
className={typeof byteClassName === 'function' ? byteClassName(byte, l * BYTES_PER_LINE + b) : byteClassName}
>
{byte.toString(16).padStart(2, '0')}
</span>
</React.Fragment>
))}
</span>
</React.Fragment>
))
}
</code>
</pre>
</div>
</div>
)
});

BinaryDataCanvas.displayName = 'BinaryDataCanvas';

+ 134
- 0
packages/web-kitchensink-reactnext/src/packages/react-prism/index.tsx View File

@@ -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<React.HTMLProps<PrismDerivedElement>, 'children'> {
code?: string;
language?: string;
lineNumbers?: boolean;
maxLineNumber?: number;
tabSize?: number;
}

export const Prism = React.forwardRef<PrismDerivedElement, PrismProps>(({
code,
language = 'plain',
lineNumbers = false,
maxLineNumber = Infinity,
tabSize = 2,
className,
style,
...etcProps
}, forwardedRef) => {
const [highlightedCode, setHighlightedCode] = React.useState<string>('');

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 (
<div
className={clsx(
'flex gap-2 leading-tight',
className,
)}
style={style}
>
{lineNumbers && (
<pre
style={{
padding: 0,
overflow: 'visible',
margin: 0,
textAlign: 'right',
}}
>
{new Array(maxLineNumber).fill(0).map((_, i) => (
<React.Fragment key={i}>
{i > 0 && '\n'}
<code
style={{
whiteSpace: 'nowrap',
}}
>
{i + 1}
</code>
</React.Fragment>
))}
</pre>
)}
<pre
{...etcProps}
ref={forwardedRef}
>
{
language !== 'plain'
&& (
<code
className="prism"
dangerouslySetInnerHTML={{
__html: highlightedCode,
}}
style={{
tabSize,
}}
/>
)
}
{
language === 'plain'
&& (
<code
className="prism"
style={{
tabSize,
}}
>
{code}
</code>
)
}
</pre>
</div>
)
});

Prism.displayName = 'Prism';


+ 2
- 1
packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/WaveformCanvas/index.tsx View File

@@ -80,6 +80,7 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent
container: containerRef.current,
height: containerRef.current.clientHeight,
autoplay: autoPlay,
fillParent: true,
waveColor,
progressColor,
cursorColor,
@@ -159,7 +160,7 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent
className="flex-auto relative"
>
<div className="absolute top-0 left-0 w-full h-full">
<div className="w-full h-full"
<div className="w-full h-full relative"
ref={containerRef}
/>
</div>


packages/web-kitchensink-reactnext/src/utils/text.ts → packages/web-kitchensink-reactnext/src/packages/text-utils/index.ts View File

@@ -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<TextMetadata> => {
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<string, number | string>
{
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);
}

packages/web-kitchensink-reactnext/src/utils/video.ts → packages/web-kitchensink-reactnext/src/packages/video-utils/index.ts View File

@@ -1,4 +1,11 @@
export const getVideoMetadata = (videoUrl: string) => new Promise<Record<string, string | number>>((resolve, reject) => {
export interface VideoFileMetadata {
width?: number;
height?: number;
duration?: number;
}

export const getVideoMetadata = (videoUrl?: string) => new Promise<VideoFileMetadata>((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<Record<string,
const metadata = {
width: thisVideo.videoWidth,
height: thisVideo.videoHeight,
duration: thisVideo.duration,
};
video.remove();
resolve(metadata);
@@ -17,6 +25,11 @@ export const getVideoMetadata = (videoUrl: string) => new Promise<Record<string,
video.remove();
});

if (typeof videoUrl === 'undefined') {
resolve({});
return;
}

source.src = videoUrl;
video.appendChild(source);
});

+ 96
- 3
packages/web-kitchensink-reactnext/src/pages/categories/blob/index.tsx View File

@@ -41,18 +41,52 @@ const BlobPage: NextPage = () => {
});
}, []);

const [binaryFile, setBinaryFile] = React.useState<File>();
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<File>();
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<File>();
React.useEffect(() => {
fetch('/code.py').then((response) => {
response.blob().then((blob) => {
setCodeFile(new File([blob], 'code.py', {
type: 'text/x-python',
}));
});
});
}, []);

return (
<DefaultLayout>
<Section title="ImageFilePreview">
<Subsection title="Single File">
<BlobReact.ImageFilePreview
enhanced
file={
imageFile
?? {
name: 'image.png',
type: 'image/png',
url: '/image.png',
}
} as Partial<File>
}
className="sm:h-64"
/>
@@ -61,7 +95,14 @@ const BlobPage: NextPage = () => {
<Section title="VideoFilePreview">
<Subsection title="Single File">
<BlobReact.VideoFilePreview
file={videoFile}
file={
videoFile
?? {
name: 'video.mp4',
type: 'video/mp4',
url: '/video.mp4',
} as Partial<File>
}
className="sm:h-64"
enhanced
/>
@@ -70,12 +111,64 @@ const BlobPage: NextPage = () => {
<Section title="AudioFilePreview">
<Subsection title="Single File">
<BlobReact.AudioFilePreview
file={audioFile}
file={
audioFile
?? {
name: 'audio.wav',
type: 'audio/wav',
url: '/audio.wav',
} as Partial<File>
}
className="sm:h-64"
enhanced
/>
</Subsection>
</Section>
<Section title="BinaryFilePreview">
<Subsection title="Single File">
<BlobReact.BinaryFilePreview
file={
binaryFile
?? {
name: 'binary.bin',
type: 'application/octet-stream',
url: '/binary.bin',
} as Partial<File>
}
className="sm:h-64"
/>
</Subsection>
</Section>
<Section title="TextFilePreview">
<Subsection title="Single File (Plaintext)">
<BlobReact.TextFilePreview
file={
plaintextFile
?? {
name: 'plaintext.txt',
type: 'text/plain',
url: '/plaintext.txt',
} as Partial<File>
}
className="sm:h-64"
/>
</Subsection>
</Section>
<Section title="TextFilePreview">
<Subsection title="Single File (Code)">
<BlobReact.TextFilePreview
file={
codeFile
?? {
name: 'code.py',
type: 'text/x-python',
url: '/code.py',
} as Partial<File>
}
className="sm:h-64"
/>
</Subsection>
</Section>
<Section title="FileSelectBox">
<Subsection title="Single File">
{/*<BlobReact.FileSelectBox*/}


+ 1
- 1
packages/web-kitchensink-reactnext/src/pages/index.tsx View File

@@ -3,7 +3,7 @@ import Link from 'next/link';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as Navigation from '@tesseract-design/web-navigation-react';
import { DefaultLayout } from 'src/components/DefaultLayout';
import { DefaultLayout } from '@/components/DefaultLayout';
import * as React from 'react'

type Page = {


+ 31
- 0
packages/web-kitchensink-reactnext/src/styles/globals.css View File

@@ -78,3 +78,34 @@
.slider:active::-webkit-slider-thumb {
box-shadow: -100000.5em 0 0 100000em rgb(var(--color-tertiary) / 50%);
}

.prism .token.number { color: rgb(var(--color-code-number)); }
.prism .token.keyword { color: rgb(var(--color-code-keyword)); }
.prism .token.type { color: rgb(var(--color-code-type)); }
.prism .token.instance-attribute { color: rgb(var(--color-code-instance-attribute)); }
.prism .token.function { color: rgb(var(--color-code-function)); }
.prism .token.parameter { color: rgb(var(--color-code-parameter)); }
.prism .token.property { color: rgb(var(--color-code-property)); }
.prism .token.string { color: rgb(var(--color-code-string)); }
.prism .token.variable { color: rgb(var(--color-code-variable)); }
.prism .token.regexp { color: rgb(var(--color-code-regexp)); }
.prism .token.url { color: rgb(var(--color-code-url)); }
.prism .token.global { color: rgb(var(--color-code-global)); }
.prism .token.comment { opacity: 0.5; }

.binary-file-preview .x00 { color: rgb(var(--color-code-keyword)); }
.binary-file-preview .x10 { color: rgb(var(--color-code-global)); }
.binary-file-preview .x20 { color: rgb(var(--color-code-string)); }
.binary-file-preview .x30 { color: rgb(var(--color-code-number)); }
.binary-file-preview .x40 { color: rgb(var(--color-code-url)); }
.binary-file-preview .x50 { color: rgb(var(--color-code-type)); }
.binary-file-preview .x60 { color: rgb(var(--color-code-parameter)); }
.binary-file-preview .x70 { color: rgb(var(--color-code-property)); }
.binary-file-preview .x80 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-keyword)); }
.binary-file-preview .x90 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-global)); }
.binary-file-preview .xa0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-string)); }
.binary-file-preview .xb0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-number)); }
.binary-file-preview .xc0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-url)); }
.binary-file-preview .xd0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-type)); }
.binary-file-preview .xe0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-parameter)); }
.binary-file-preview .xf0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-property)); }

+ 1
- 1
packages/web-kitchensink-reactnext/src/styles/theme.ts View File

@@ -16,7 +16,7 @@ const theme = {
"code-variable": "139 194 117",
"code-regexp": "116 167 43",
"code-url": "0 153 204",
"code-global": "194 128 80"
"code-global": "194 128 80",
} as const;

export default theme;

+ 49
- 129
packages/web-kitchensink-reactnext/src/utils/blob.ts View File

@@ -1,13 +1,8 @@
import * as mimeTypes from 'mime-types';
import Blob from '../pages/categories/blob';
import {getTextMetadata} from './text';
import {getImageMetadata} from './image';
import {getAudioMetadata} from './audio';
import {getVideoMetadata} from '@/utils/video';

export interface FallbackFile extends Partial<File> {
metadata?: Record<string, unknown>;
}
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>) => blob?.text?.();

export const readAsDataURL = (blob: Partial<Blob>) => new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('error', () => {
@@ -135,57 +128,6 @@ export const readAsDataURL = (blob: Partial<Blob>) => new Promise<string>((resol
reader.readAsDataURL(blob as Blob);
});

export const readAsArrayBuffer = (blob: Blob) => blob.arrayBuffer();

interface FileWithResolvedType<T extends ContentType> extends Partial<FileWithDataUrl> {
resolvedType: T;
originalFile?: File;
}

export interface TextFileMetadata {
contents?: string;
scheme?: string;
schemeTitle?: string;
language?: string;
languageProbability?: number;
}

export interface TextFile extends FileWithResolvedType<ContentType.TEXT> {
metadata?: TextFileMetadata;
}

const augmentTextFile = async (f: File): Promise<TextFile> => {
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<ContentType.IMAGE> {
metadata?: ImageFileMetadata;
}

export interface FileWithDataUrl extends File {
url?: string;
}
@@ -195,94 +137,72 @@ export const addDataUrl = async (f: Partial<File>): Promise<Partial<FileWithData
return f;
}

export const augmentImageFile = async (f: FileWithDataUrl): Promise<ImageFile> => {
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<FileWithDataUrl> {
resolvedType: ContentType;
}

export interface TextFile extends FileWithResolvedContentType {
resolvedType: ContentType.TEXT;
metadata?: TextMetadata;
}

export const augmentTextFile = async <T extends Partial<FileWithDataUrl>>(file: T): Promise<TextFile> => {
const contents = typeof file?.text === 'function' ? await file.text() : '';
const fileMutable = file as unknown as Record<string, TextMetadata>;
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<ContentType.AUDIO> {
metadata?: AudioFileMetadata;
export const augmentImageFile = async <T extends Partial<FileWithDataUrl>>(file: T): Promise<ImageFile> => {
const fileMutable = file as unknown as Record<string, ImageMetadata>;
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<AudioFile> => {
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 <T extends Partial<FileWithDataUrl>>(file: T): Promise<AudioFile> => {
const fileMutable = file as unknown as Record<string, AudioMetadata>;
fileMutable.metadata = await getAudioMetadata(file.url, file.type);
return fileMutable as unknown as AudioFile;
};

export interface BinaryFileMetadata {
contents: ArrayBuffer;
}

export interface BinaryFile extends FileWithResolvedType<ContentType.BINARY> {
export interface BinaryFile extends FileWithResolvedContentType {
resolvedType: ContentType.BINARY;
metadata?: BinaryFileMetadata;
}

const augmentBinaryFile = async (f: File): Promise<BinaryFile> => {
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 <T extends Partial<FileWithDataUrl>>(file: T): Promise<BinaryFile> => {
const metadata = {} as BinaryFileMetadata;
if (typeof file?.arrayBuffer === 'function') {
metadata.contents = await file.arrayBuffer();
}
const fileMutable = file as unknown as Record<string, BinaryFileMetadata>;
fileMutable.metadata = metadata;
return fileMutable as unknown as BinaryFile;
};

export interface VideoFileMetadata {
previewUrl?: string;
width?: number;
height?: number;
}

export interface VideoFile extends FileWithResolvedType<ContentType.VIDEO> {
export interface VideoFile extends FileWithResolvedContentType {
resolvedType: ContentType.VIDEO;
metadata?: VideoFileMetadata;
}

export const augmentVideoFile = async (f: File): Promise<VideoFile> => {
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 <T extends Partial<FileWithDataUrl>>(file: T): Promise<VideoFile> => {
const fileMutable = file as unknown as Record<string, AudioMetadata>;
fileMutable.metadata = await getVideoMetadata(file.url);
return fileMutable as unknown as VideoFile;
};

export type AugmentedFile = TextFile | ImageFile | AudioFile | VideoFile | BinaryFile;


+ 1
- 1
packages/web-kitchensink-reactnext/src/utils/numeral.ts View File

@@ -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)) {


+ 1
- 0
packages/web-kitchensink-reactnext/tsconfig.json View File

@@ -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"],


Loading…
Cancel
Save