mirror of
https://github.com/urbit/shrub.git
synced 2024-12-19 08:32:39 +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';
|
} from '~/views/apps/links/components/LinkInput';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Input/Collections',
|
title: 'Collections/Input',
|
||||||
component: LinkInput,
|
component: LinkInput,
|
||||||
decorators: [withDesign]
|
decorators: [withDesign]
|
||||||
} as Meta;
|
} 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;
|
showArrow: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i);
|
export const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i);
|
||||||
const AUDIO_REGEX = new RegExp(/(mp3|wav|ogg)$/i);
|
export const AUDIO_REGEX = new RegExp(/(mp3|wav|ogg|m4a)$/i);
|
||||||
const VIDEO_REGEX = new RegExp(/(mov|mp4|ogv)$/i);
|
export const VIDEO_REGEX = new RegExp(/(mov|mp4|ogv)$/i);
|
||||||
|
|
||||||
const TruncatedText = styled(Text)`
|
const TruncatedText = styled(Text)`
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
Loading…
Reference in New Issue
Block a user