Sortable playlist (#135)

* Sortable playlist
* Year field
This commit is contained in:
Ilya Y 2023-07-18 22:11:00 +03:00 committed by GitHub
parent 9b18587c9e
commit 6bce7d576c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 85 additions and 13 deletions

View File

@ -167,6 +167,8 @@
"More": "More", "More": "More",
"Most commented": "Commented", "Most commented": "Commented",
"Most read": "Readable", "Most read": "Readable",
"Move down": "Move down",
"Move up": "Move up",
"My feed": "My feed", "My feed": "My feed",
"My subscriptions": "Subscriptions", "My subscriptions": "Subscriptions",
"Name": "Name", "Name": "Name",

View File

@ -177,6 +177,8 @@
"More": "Ещё", "More": "Ещё",
"Most commented": "Комментируемое", "Most commented": "Комментируемое",
"Most read": "Читаемое", "Most read": "Читаемое",
"Move down": "Переместить вниз",
"Move up": "Переместить вверх",
"My feed": "Моя лента", "My feed": "Моя лента",
"My subscriptions": "Подписки", "My subscriptions": "Подписки",
"Name": "Имя", "Name": "Имя",

View File

@ -309,8 +309,25 @@ $vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thum
.actions { .actions {
margin-left: auto; margin-left: auto;
display: flex; display: flex;
align-items: center;
flex-direction: row; flex-direction: row;
gap: 16px; gap: 16px;
.action {
border: 1px solid transparent;
&:not([disabled]):hover {
border-color: var(--background-color-invert);
background: var(--background-color-invert);
img {
filter: var(--icon-filter-hover);
}
}
}
.moveIconDown {
transform: rotate(180deg);
}
} }
.descriptionBlock { .descriptionBlock {

View File

@ -11,6 +11,7 @@ type Props = {
body?: string body?: string
editorMode?: boolean editorMode?: boolean
onMediaItemFieldChange?: (index: number, field: keyof MediaItem, value: string) => void onMediaItemFieldChange?: (index: number, field: keyof MediaItem, value: string) => void
onChangeMediaIndex?: (direction: 'up' | 'down', index) => void
} }
const getFormattedTime = (point) => new Date(point * 1000).toISOString().slice(14, -5) const getFormattedTime = (point) => new Date(point * 1000).toISOString().slice(14, -5)
@ -159,6 +160,7 @@ export const AudioPlayer = (props: Props) => {
<PlayerPlaylist <PlayerPlaylist
editorMode={props.editorMode} editorMode={props.editorMode}
onPlayMedia={handlePlayMedia} onPlayMedia={handlePlayMedia}
onChangeMediaIndex={(direction, index) => props.onChangeMediaIndex(direction, index)}
isPlaying={isPlaying()} isPlaying={isPlaying()}
media={props.media} media={props.media}
currentTrackIndex={currentTrackIndex()} currentTrackIndex={currentTrackIndex()}

View File

@ -18,6 +18,7 @@ type Props = {
body?: string body?: string
editorMode?: boolean editorMode?: boolean
onMediaItemFieldChange?: (index: number, field: keyof MediaItem, value: string) => void onMediaItemFieldChange?: (index: number, field: keyof MediaItem, value: string) => void
onChangeMediaIndex?: (direction: 'up' | 'down', index) => void
} }
export const PlayerPlaylist = (props: Props) => { export const PlayerPlaylist = (props: Props) => {
@ -72,6 +73,34 @@ export const PlayerPlaylist = (props: Props) => {
</Show> </Show>
</div> </div>
<div class={styles.actions}> <div class={styles.actions}>
<Show when={props.editorMode}>
<Popover content={t('Move up')}>
{(triggerRef: (el) => void) => (
<button
type="button"
ref={triggerRef}
class={styles.action}
disabled={index() === 0}
onClick={() => props.onChangeMediaIndex('up', index())}
>
<Icon name="up-button" />
</button>
)}
</Popover>
<Popover content={t('Move down')}>
{(triggerRef: (el) => void) => (
<button
type="button"
ref={triggerRef}
class={styles.action}
disabled={index() === props.media.length - 1}
onClick={() => props.onChangeMediaIndex('down', index())}
>
<Icon name="up-button" class={styles.moveIconDown} />
</button>
)}
</Popover>
</Show>
<Show when={(mi.lyrics || mi.body) && !props.editorMode}> <Show when={(mi.lyrics || mi.body) && !props.editorMode}>
<Popover content={t('Show lyrics')}> <Popover content={t('Show lyrics')}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el) => void) => (

View File

@ -190,12 +190,6 @@ export const FullArticle = (props: ArticleProps) => {
</div> </div>
</Show> </Show>
<Show when={media().length > 0 && props.article.layout === 'audio'}>
<div class="media-items">
<AudioPlayer media={media()} articleSlug={props.article.slug} body={body()} />
</div>
</Show>
<Show when={body()}> <Show when={body()}>
<div class={styles.shoutBody}> <div class={styles.shoutBody}>
<Show when={!body().startsWith('<')} fallback={<div innerHTML={body()} />}> <Show when={!body().startsWith('<')} fallback={<div innerHTML={body()} />}>

View File

@ -2,8 +2,8 @@ import { clsx } from 'clsx'
import styles from './AudioUploader.module.scss' import styles from './AudioUploader.module.scss'
import { DropArea } from '../../_shared/DropArea' import { DropArea } from '../../_shared/DropArea'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { createEffect, createSignal, on, Show } from 'solid-js' import { Show } from 'solid-js'
import { MediaItem, UploadedFile } from '../../../pages/types' import { MediaItem } from '../../../pages/types'
import { composeMediaItems } from '../../../utils/composeMediaItems' import { composeMediaItems } from '../../../utils/composeMediaItems'
import { AudioPlayer } from '../../Article/AudioPlayer' import { AudioPlayer } from '../../Article/AudioPlayer'
import { Buffer } from 'buffer' import { Buffer } from 'buffer'
@ -20,6 +20,7 @@ type Props = {
} }
onAudioChange: (index: number, value: MediaItem) => void onAudioChange: (index: number, value: MediaItem) => void
onAudioAdd: (value: MediaItem[]) => void onAudioAdd: (value: MediaItem[]) => void
onAudioSorted: (value: MediaItem[]) => void
} }
export const AudioUploader = (props: Props) => { export const AudioUploader = (props: Props) => {
@ -29,6 +30,18 @@ export const AudioUploader = (props: Props) => {
props.onAudioChange(index, { ...props.audio[index], [field]: value }) props.onAudioChange(index, { ...props.audio[index], [field]: value })
} }
const handleChangeIndex = (direction: 'up' | 'down', index: number) => {
const media = [...props.audio]
if (direction === 'up' && index > 0) {
const copy = media.splice(index, 1)[0]
media.splice(index - 1, 0, copy)
} else if (direction === 'down' && index < media.length - 1) {
const copy = media.splice(index, 1)[0]
media.splice(index + 1, 0, copy)
}
props.onAudioSorted(media)
}
return ( return (
<div class={clsx(styles.AudioUploader, props.class)}> <div class={clsx(styles.AudioUploader, props.class)}>
<Show when={props.audio.length > 0}> <Show when={props.audio.length > 0}>
@ -36,6 +49,7 @@ export const AudioUploader = (props: Props) => {
editorMode={true} editorMode={true}
media={props.audio} media={props.audio}
onMediaItemFieldChange={handleMediaItemFieldChange} onMediaItemFieldChange={handleMediaItemFieldChange}
onChangeMediaIndex={handleChangeIndex}
/> />
</Show> </Show>
<DropArea <DropArea

View File

@ -96,6 +96,13 @@
.additionalInput { .additionalInput {
@include font-size(1.4rem); @include font-size(1.4rem);
-moz-appearance: textfield;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
font-weight: 600; font-weight: 600;
padding: 0; padding: 0;
margin: 14px 0 0; margin: 14px 0 0;

View File

@ -1,4 +1,4 @@
import { Accessor, createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from 'solid-js' import { Accessor, createMemo, createSignal, For, onCleanup, onMount, Show } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Title } from '@solidjs/meta' import { Title } from '@solidjs/meta'
@ -132,7 +132,7 @@ export const EditView = (props: Props) => {
const newMedia = [...mediaItems(), ...data] const newMedia = [...mediaItems(), ...data]
setForm('media', JSON.stringify(newMedia)) setForm('media', JSON.stringify(newMedia))
} }
const handleSortedImages = (data) => { const handleSortedMedia = (data) => {
setForm('media', JSON.stringify(data)) setForm('media', JSON.stringify(data))
} }
@ -244,6 +244,10 @@ export const EditView = (props: Props) => {
onChange={(event) => handleBaseFieldsChange('artist', event.target.value)} onChange={(event) => handleBaseFieldsChange('artist', event.target.value)}
/> />
<input <input
type="number"
min="1900"
max={new Date().getFullYear()}
step="1"
class={styles.additionalInput} class={styles.additionalInput}
placeholder={t('Release date...')} placeholder={t('Release date...')}
value={mediaItems()[0]?.date || ''} value={mediaItems()[0]?.date || ''}
@ -305,7 +309,7 @@ export const EditView = (props: Props) => {
onImageChange={handleMediaChange} onImageChange={handleMediaChange}
onImageDelete={(index) => handleImageDelete(index)} onImageDelete={(index) => handleImageDelete(index)}
onImagesAdd={(value) => handleAddMedia(value)} onImagesAdd={(value) => handleAddMedia(value)}
onImagesSorted={(value) => handleSortedImages(value)} onImagesSorted={(value) => handleSortedMedia(value)}
/> />
</Show> </Show>
@ -341,6 +345,7 @@ export const EditView = (props: Props) => {
baseFields={baseAudioFields()} baseFields={baseAudioFields()}
onAudioAdd={(value) => handleAddMedia(value)} onAudioAdd={(value) => handleAddMedia(value)}
onAudioChange={handleMediaChange} onAudioChange={handleMediaChange}
onAudioSorted={(value) => handleSortedMedia(value)}
/> />
</Show> </Show>
@ -387,7 +392,7 @@ export const EditView = (props: Props) => {
{/* />*/} {/* />*/}
{/*</div>*/} {/*</div>*/}
<h4>Темы</h4> <h4>{t('Topics')}</h4>
{/*<p class="description">*/} {/*<p class="description">*/}
{/* Добавьте несколько тем, чтобы читатель знал, о&nbsp;чем ваш материал, и&nbsp;мог найти*/} {/* Добавьте несколько тем, чтобы читатель знал, о&nbsp;чем ваш материал, и&nbsp;мог найти*/}
{/* его на&nbsp;страницах интересных ему тем. Темы можно менять местами, первая тема*/} {/* его на&nbsp;страницах интересных ему тем. Темы можно менять местами, первая тема*/}

View File

@ -295,7 +295,7 @@ export const SolidSwiper = (props: Props) => {
</div> </div>
<div <div
class={clsx(styles.action, { class={clsx(styles.action, {
[styles.hidden]: index() + 1 === Number(props.images.length) [styles.hidden]: index() === props.images.length - 1
})} })}
onClick={() => handleChangeIndex('right', index())} onClick={() => handleChangeIndex('right', index())}
> >