mirror of
https://github.com/urbit/shrub.git
synced 2024-12-22 10:21:31 +03:00
collections: add LinkBlockItem
This commit is contained in:
parent
0353fcf6f6
commit
5f8249fab0
76
pkg/interface/src/stories/LinkBlockItem.stories.tsx
Normal file
76
pkg/interface/src/stories/LinkBlockItem.stories.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { Meta } from '@storybook/react';
|
||||
import { withDesign } from 'storybook-addon-designs';
|
||||
|
||||
import { Col } from '@tlon/indigo-react';
|
||||
import { LinkBlockItem } from '~/views/apps/links/components/LinkBlockItem';
|
||||
|
||||
export default {
|
||||
title: 'Collections/BlockItem',
|
||||
component: LinkBlockItem,
|
||||
decorators: [withDesign]
|
||||
} as Meta;
|
||||
|
||||
export const Image = () => (
|
||||
<Col gapY="2" p="2" width="500px" backgroundColor="white">
|
||||
<LinkBlockItem url="https://media.urbit.org/site/posts/essays/value-of-address-space-pt1.jpg" />
|
||||
<LinkBlockItem url="https://media.urbit.org/site/posts/essays/ocean.jpeg" />
|
||||
</Col>
|
||||
);
|
||||
|
||||
Image.parameters = {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url:
|
||||
'https://www.figma.com/file/ovD1mlsYDa0agyYTdvCmGr/Landscape?node-id=8228%3A11'
|
||||
}
|
||||
};
|
||||
|
||||
export const Fallback = () => (
|
||||
<Col gapY="2" p="2" width="500px" backgroundColor="white">
|
||||
<LinkBlockItem url="https://www.are.na/edouard-urcades/edouard" />
|
||||
<LinkBlockItem url="https://thejaymo.net" />
|
||||
</Col>
|
||||
);
|
||||
|
||||
Fallback.parameters = {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url:
|
||||
'https://www.figma.com/file/ovD1mlsYDa0agyYTdvCmGr/Landscape?node-id=8228%3A57'
|
||||
}
|
||||
};
|
||||
|
||||
export const Audio = () => (
|
||||
<Col gapY="2" p="2" width="500px" backgroundColor="white">
|
||||
<LinkBlockItem
|
||||
title="Artist · Track"
|
||||
url="https://rovnys-public.s3.amazonaws.com/urbit-from-the-outside-in-1.m4a"
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
|
||||
Audio.parameters = {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url:
|
||||
'https://www.figma.com/file/ovD1mlsYDa0agyYTdvCmGr/Landscape?node-id=8229%3A0'
|
||||
}
|
||||
};
|
||||
|
||||
export const Youtube = () => (
|
||||
<Col gapY="2" p="2" width="500px" backgroundColor="white">
|
||||
<LinkBlockItem
|
||||
title="Artist · Track"
|
||||
url="https://www.youtube.com/watch?v=M04AKTCDavc&t=1s"
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
|
||||
Youtube.parameters = {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url:
|
||||
'https://www.figma.com/file/ovD1mlsYDa0agyYTdvCmGr/Landscape?node-id=8229%3A0'
|
||||
}
|
||||
};
|
@ -9,7 +9,7 @@ import {
|
||||
} from '~/views/apps/links/components/LinkInput';
|
||||
|
||||
export default {
|
||||
title: 'Input/Collections',
|
||||
title: 'Collections/Input',
|
||||
component: LinkInput,
|
||||
decorators: [withDesign]
|
||||
} as Meta;
|
||||
|
@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
BaseImage,
|
||||
Icon,
|
||||
Center,
|
||||
Row,
|
||||
Text
|
||||
} from '@tlon/indigo-react';
|
||||
import { AUDIO_REGEX, IMAGE_REGEX } from '~/views/components/RemoteContent';
|
||||
import { AudioPlayer } from '~/views/components/AudioPlayer';
|
||||
|
||||
export interface LinkBlockItemProps {
|
||||
url: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
function getYoutubeId(str: string): string | null {
|
||||
const youtube = str.match(/youtube\.com.*(\?v=|\/embed\/)(.{11})/);
|
||||
if(!youtube) {
|
||||
return null;
|
||||
}
|
||||
return youtube.pop();
|
||||
}
|
||||
|
||||
export function LinkBlockItem(props: LinkBlockItemProps) {
|
||||
const { url, title } = props;
|
||||
|
||||
const isImage = IMAGE_REGEX.test(url);
|
||||
const isAudio = AUDIO_REGEX.test(url);
|
||||
const youtube = getYoutubeId(url);
|
||||
return (
|
||||
<Center
|
||||
border="1"
|
||||
borderColor="lightGray"
|
||||
borderRadius="1"
|
||||
height="256px"
|
||||
width="256px"
|
||||
>
|
||||
{isImage ? (
|
||||
<BaseImage
|
||||
style={{ objectFit: 'contain' }}
|
||||
height="100%"
|
||||
src={url}
|
||||
width="100%"
|
||||
/>
|
||||
) : isAudio ? (
|
||||
<AudioPlayer title={title} url={url} />
|
||||
) : youtube ? (
|
||||
<BaseImage
|
||||
style={{ objectFit: 'contain' }}
|
||||
height="100%"
|
||||
src={`https://img.youtube.com/vi/${youtube}/${0}.jpg`}
|
||||
width="100%"
|
||||
/>
|
||||
) : (
|
||||
<Row overflow="hidden" gapX="2" alignItems="center" p="2">
|
||||
<Icon color="gray" icon="ArrowExternal" />
|
||||
<Text
|
||||
gray
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
>
|
||||
{url}
|
||||
</Text>
|
||||
</Row>
|
||||
)}
|
||||
</Center>
|
||||
);
|
||||
}
|
77
pkg/interface/src/views/components/AudioPlayer.tsx
Normal file
77
pkg/interface/src/views/components/AudioPlayer.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Action, Text, Icon, Row } from '@tlon/indigo-react';
|
||||
|
||||
function formatTime(num: number) {
|
||||
const minutes = Math.floor(num / 60);
|
||||
const seconds = Math.floor(num % 60);
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function AudioPlayer(props: { url: string; title?: string }) {
|
||||
const { url, title = '' } = props;
|
||||
const ref = useRef<HTMLAudioElement>();
|
||||
|
||||
const [playing, setPlaying] = useState(false);
|
||||
|
||||
const playPause = useCallback(() => {
|
||||
if (playing) {
|
||||
ref.current.pause();
|
||||
} else {
|
||||
ref.current.play();
|
||||
}
|
||||
setPlaying(p => !p);
|
||||
}, [ref, playing]);
|
||||
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (playing) {
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function updateProgress() {
|
||||
setProgress(ref.current.currentTime);
|
||||
}
|
||||
|
||||
const tim = setInterval(updateProgress, 250);
|
||||
|
||||
return () => {
|
||||
tim && clearTimeout(tim);
|
||||
};
|
||||
}
|
||||
}, [ref.current, playing]);
|
||||
|
||||
useEffect(() => {
|
||||
ref.current.addEventListener('loadedmetadata', () => {
|
||||
setDuration(ref.current.duration);
|
||||
});
|
||||
}, [ref.current]);
|
||||
|
||||
return (
|
||||
<Row
|
||||
backgroundColor="white"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
height="100%"
|
||||
width="100%"
|
||||
position="relative"
|
||||
>
|
||||
<Row p="2" position="absolute" left="0" top="0">
|
||||
<Text lineHeight="tall">{title}</Text>
|
||||
</Row>
|
||||
<audio ref={ref} src={url} preload="metadata" />
|
||||
<Action backgroundColor="white" height="unset" onClick={playPause}>
|
||||
<Icon
|
||||
height="32px"
|
||||
width="32px"
|
||||
color="black"
|
||||
icon={playing ? 'LargeBullet' : 'TriangleEast'}
|
||||
/>
|
||||
</Action>
|
||||
<Row p="2" position="absolute" right="0" bottom="0">
|
||||
<Text lineHeight="tall">
|
||||
{formatTime(progress)} / {formatTime(duration)}
|
||||
</Text>
|
||||
</Row>
|
||||
</Row>
|
||||
);
|
||||
}
|
@ -32,9 +32,9 @@ interface RemoteContentState {
|
||||
showArrow: boolean;
|
||||
}
|
||||
|
||||
const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i);
|
||||
const AUDIO_REGEX = new RegExp(/(mp3|wav|ogg)$/i);
|
||||
const VIDEO_REGEX = new RegExp(/(mov|mp4|ogv)$/i);
|
||||
export const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i);
|
||||
export const AUDIO_REGEX = new RegExp(/(mp3|wav|ogg|m4a)$/i);
|
||||
export const VIDEO_REGEX = new RegExp(/(mov|mp4|ogv)$/i);
|
||||
|
||||
const TruncatedText = styled(Text)`
|
||||
white-space: pre;
|
||||
|
Loading…
Reference in New Issue
Block a user