Merging develop

This commit is contained in:
James Barnsley
2022-09-04 09:13:45 +12:00
21 changed files with 2962 additions and 2967 deletions

5569
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,86 @@
import React, { useState, useEffect } from 'react';
import { ReactSortable } from 'react-sortablejs';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useDrop, useDrag } from 'react-dnd';
import Icon from '../Icon';
import Link from '../Link';
import { sortItems, indexToArray } from '../../util/arrays';
const DRAGGABLE_TYPE = 'COMMAND';
const Command = ({
index,
onExecute,
onDrag,
onDragEnd,
command: {
id,
url,
name,
icon,
colour,
} = {},
}) => {
const ref = useRef(null);
const [{ isDragging }, dragRef] = useDrag({
type: DRAGGABLE_TYPE,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
end: onDragEnd,
});
const [_dropProps, dropRef] = useDrop({
accept: DRAGGABLE_TYPE,
hover: (item, monitor) => {
const dragIndex = item.index;
const hoverIndex = index;
const hoverBoundingRect = ref.current?.getBoundingClientRect()
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const hoverActualY = monitor.getClientOffset().y - hoverBoundingRect.top;
// if dragging down, continue only when hover is smaller than middle Y
if (dragIndex < hoverIndex && hoverActualY < hoverMiddleY) return;
// if dragging up, continue only when hover is bigger than middle Y
if (dragIndex > hoverIndex && hoverActualY > hoverMiddleY) return;
onDrag(dragIndex, hoverIndex);
item.index = hoverIndex;
},
})
let className = 'list__item commands-setup__item';
if (isDragging) className += ' list__item--dragging';
return (
<div className={className} ref={dropRef(ref)}>
<div className="commands-setup__item__details">
<div ref={dragRef}>
<Icon
className="commands-setup__item__drag-handle"
name="drag_indicator"
/>
</div>
<div className="commands-setup__item__command-item commands__item commands__item--small">
<Icon className="commands__item__icon" name={icon} />
<span className={`${colour}-background commands__item__background`} />
</div>
<div className="commands-setup__item__url commands__item__url">
{name || <span className="grey-text">{url}</span>}
</div>
</div>
<div className="commands-setup__item__actions">
<a className="commands-setup__item__run-button action" onClick={() => onExecute(id, true)}>
<Icon name="play_arrow" />
</a>
<Link className="commands-setup__item__edit-button action" to={`/modal/edit-command/${id}`}>
<Icon name="edit" />
</Link>
</div>
</div>
);
}
const Commands = ({
commands,
runCommand,
@ -18,58 +95,44 @@ const Commands = ({
useEffect(() => {
setList(sortItems(indexToArray(commands), 'sort_order'));
}, []);
const onSort = () => {
const nextCommands = {};
if (!list || !list.length) return;
const onDrag = useCallback((dragIndex, hoverIndex) => {
setList((prev) => {
const next = [...prev];
const dragItem = next[dragIndex];
const hoverItem = next[hoverIndex];
next[dragIndex] = hoverItem;
next[hoverIndex] = dragItem;
return next;
});
}, [list]);
const onDragEnd = () => {
if (!list || !list.length) return;
const nextCommands = {};
list.forEach((item, index) => {
nextCommands[item.id] = {
...item,
sort_order: index,
};
});
onChange(nextCommands);
};
return (
<ReactSortable
options={{
handle: '.commands-setup__item__drag-handle',
animation: 150,
}}
className="list commands-setup"
list={list}
setList={setList}
onSort={onSort}
>
{
list.map((command) => (
<div className="list__item commands-setup__item list__item--no-interaction" key={command.id} data-id={command.id}>
<div className="commands-setup__item__details">
<Icon className="commands-setup__item__drag-handle" name="drag_indicator" />
<div className="commands-setup__item__command-item commands__item commands__item--small">
<Icon className="commands__item__icon" name={command.icon} />
<span className={`${command.colour}-background commands__item__background`} />
</div>
<div className="commands-setup__item__url commands__item__url">
{command.name ? command.name : <span className="grey-text">{command.url}</span>}
</div>
</div>
<div className="commands-setup__item__actions">
<a className="commands-setup__item__run-button action" onClick={() => runCommand(command.id, true)}>
<Icon name="play_arrow" />
</a>
<Link className="commands-setup__item__edit-button action" to={`/modal/edit-command/${command.id}`}>
<Icon name="edit" />
</Link>
</div>
</div>
))
}
</ReactSortable>
<div className="commands-setup__item__drag-handle list commands-setup">
{list.map((command, index) => (
<Command
key={`command_${command.id}`}
index={index}
command={command}
onExecute={runCommand}
onDrag={onDrag}
onDragEnd={onDragEnd}
/>
))}
</div>
);
}
};
export default Commands;

View File

@ -1,67 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { ReactSortable } from 'react-sortablejs';
import { set as uiSet } from '../../services/ui/actions';
import Icon from '../Icon';
import { titleCase } from '../../util/helpers';
const SourcesPriority = ({
uri_schemes,
uri_schemes_priority,
}) => {
const [list, setList] = useState([]);
const dispatch = useDispatch();
useEffect(() => {
processList();
}, []);
useEffect(() => {
processList();
}, [uri_schemes]);
const processList = () => {
let seen = [];
let unseen = [];
uri_schemes.forEach((uri) => {
const index = uri_schemes_priority.indexOf(uri);
if (index > -1) {
seen[index] = { uri };
} else {
unseen.push({ uri });
}
});
setList([ ...seen, ...unseen ]);
}
const onSort = () => {
dispatch(uiSet({ uri_schemes_priority: list.map(({ uri }) => uri) }));
}
return (
<ReactSortable
options={{
animation: 150,
}}
className="sources-priority-field"
list={list}
setList={setList}
onSort={onSort}
>
{
list.map(({ uri }) => {
const name = titleCase(uri.replace(':', '').replace('+', ' '));
return (
<span className="source flag flag--grey" key={`uri_scheme_${uri}`}>
<Icon name="drag_indicator" />
{name}
</span>
);
})
}
</ReactSortable>
);
}
export default SourcesPriority;

View File

@ -99,6 +99,36 @@ const Hotkeys = () => {
});
});
useHotkeys('1', (e) => {
prepare({
e,
label: 'Now playing',
callback: () => {
history.push('/queue');
},
});
});
useHotkeys('2', (e) => {
prepare({
e,
label: 'Search',
callback: () => {
history.push('/search');
},
});
});
useHotkeys('3', (e) => {
prepare({
e,
label: 'Kiosk mode',
callback: () => {
history.push('/modal/kiosk-mode');
},
});
});
useHotkeys('space,p', (e) => prepare({
e,
label: 'Play/pause',

View File

@ -303,9 +303,6 @@ settings:
touch_events: Aktivieren Berührungsereignissen zur Steuerungen
touch_events_tooltip: Ermöglicht Links- und Rechts-Wischen zum Wechseln der Titeln
wide_scrollbars: Breite Bildlaufleisten verwenden
sources_priority:
label: Quellen Priorität
description: Drag-and-Drop zur Priorisierung von Suchanbietern und Ergebnissen
reporting:
label: Berichterstattung
sublabel: Erlaubt die Berichterstattung anonymer Nutzungsstatistiken

View File

@ -394,9 +394,6 @@ settings:
wide: Wide scrollbars
hidden: Hidden scrollbars
smooth_scrolling: Smooth scroll
sources_priority:
label: Sources priority
description: Drag-and-drop to prioritize search providers and results
reporting:
label: Reporting
sublabel: Allow reporting of anonymous usage statistics
@ -635,6 +632,9 @@ modal:
title: Hotkeys
keys:
info: Hotkeys info (this dialog)
now_playing: Now playing
search: Search
kiosk_mode: Kiosk mode
exit: Cancel the current interaction
play_pause: Play/pause
stop: Stop

View File

@ -366,9 +366,6 @@ settings:
wide_scrollbars: Usar barras de desplazamiento anchas
grid_glow: Efecto resplandeciente en miniaturas
grid_glow_tooltip: Deshabilitar el efecto para dispositivos lentos o navegadores antiguos
sources_priority:
label: Prioridad de fuentes
description: Arrastre y suelte para priorizar proveedores y resultados
reporting:
label: Reporte
sublabel: Permitir reporte de estadísticas de uso anónimas

View File

@ -379,9 +379,6 @@ settings:
wide: Larges
hidden: Cachées
smooth_scrolling: Défilement doux
sources_priority:
label: Priorité des sources
description: Glisser-déposer pour réordonner les sources et les résultats
reporting:
label: Rapport
sublabel: Autoriser la création de rapports anonymisés sur les statistiques d'utilisation

View File

@ -366,9 +366,6 @@ settings:
wide_scrollbars: Usa barra di scorrimento larga
grid_glow: Effetto bagliore miniature
grid_glow_tooltip: Disattiva l'effetto per dispositivi poco potenti o browser non recenti
sources_priority:
label: Priorità fonti
description: Trascina e rilascia per dare la priorità ai provider di ricerca e ai risultati
reporting:
label: Segnalazione
sublabel: Consenti statistiche di utilizzo anonime

View File

@ -331,9 +331,6 @@ settings:
touch_events: 再生コントロールにタッチイベントを有効化
touch_events_tooltip: 左右スワイプでトラックを変更可能
wide_scrollbars: 広いスクロールバーを使用
sources_priority:
label: ソース優先順位
description: ソースの優先順位をドラッグ&ドロップで決める
reporting:
label: リポート
sublabel: 匿名で使用レポートを送信

View File

@ -364,9 +364,6 @@ settings:
touch_events_tooltip: Maakt het mogelijk om naar links of rechts te vegen om van tracks te veranderen
grid_glow: Miniatuur gloei effect
wide_scrollbars: Gebruik brede schuifbalken
sources_priority:
label: Prioriteit bronnen
description: Verslepen om voorrang van zoekmachines en resultaten te wijzigen
reporting:
label: Rapportering
sublabel: Sta het versturen van anonieme gebruiksstatistieken toe

View File

@ -331,9 +331,6 @@ settings:
touch_events: Włącz obsługę zdarzeń dotykowych
touch_events_tooltip: Umożliwia zmienianie utworów poprzez przesunięcia palcem po ekranie
wide_scrollbars: Używaj szerokich pasków przewijania
sources_priority:
label: Priorytety źródeł
description: Przesuń i upuść aby zmienić priorytety dostawców
reporting:
label: Raportowanie
sublabel: Włącz anonimowe raportowanie statystyk

View File

@ -391,9 +391,6 @@ settings:
wide: Широкие полосы прокрутки
hidden: Скрытые полосы прокрутки
smooth_scrolling: Плавная прокрутка
sources_priority:
label: Приоритет источников
description: Перетаскивание для определения приоритетов поисковых систем и результатов
reporting:
label: Отчетность
sublabel: Разрешить отчеты об анонимной статистике использования

View File

@ -367,9 +367,6 @@ settings:
wide_scrollbars: Use wide scrollbars
grid_glow: Thumbnail glow effect
grid_glow_tooltip: Inaktivera effekten för lågenergi enheter eller äldre webbläsare
sources_priority:
label: Sources priority
description: Dra och släpp för att prioritera sök resultaten från olika utgivningar
reporting:
label: Rapportering
sublabel: Tillåt rapportering av anonym användarstatistik

View File

@ -4,6 +4,7 @@ import { bindActionCreators } from 'redux';
import Header from '../components/Header';
import Icon from '../components/Icon';
import Button from '../components/Button';
import LinksSentence from '../components/LinksSentence';
import * as uiActions from '../services/ui/actions';
import * as pusherActions from '../services/pusher/actions';
import * as mopidyActions from '../services/mopidy/actions';
@ -75,6 +76,7 @@ class Debug extends React.Component {
log_pusher,
log_snapcast,
access_token,
uri_schemes = [],
} = this.props;
const {
mopidy_call,
@ -216,6 +218,14 @@ class Debug extends React.Component {
</label>
<h4 className="underline"><I18n path="services.mopidy.title" /></h4>
<label className="field">
<div className="name">Enabled sources</div>
<div className="input">
<span className="text">
<LinksSentence items={uri_schemes.map((name) => ({ name, uri: name }))} nolinks />
</span>
</div>
</label>
<form onSubmit={this.callMopidy}>
<label className="field">
<div className="name"><I18n path="debug.call" /></div>
@ -336,6 +346,7 @@ const mapStateToProps = (state) => ({
test_mode: (state.ui.test_mode ? state.ui.test_mode : false),
debug_info: (state.ui.debug_info ? state.ui.debug_info : false),
debug_response: state.ui.debug_response,
uri_schemes: state.mopidy.uri_schemes,
});
const mapDispatchToProps = (dispatch) => ({

View File

@ -6,6 +6,10 @@ import * as uiActions from '../../services/ui/actions';
import { I18n, i18n } from '../../locale';
const hotkeys = [
{ label: 'now_playing', keysets: [['1']] },
{ label: 'search', keysets: [['2']] },
{ label: 'kiosk_mode', keysets: [['3']] },
{ label: 'info', keysets: [['i']] },
{ label: 'info', keysets: [['i']] },
{ label: 'play_pause', keysets: [['p'], ['spacebar']] },
{ label: 'stop', keysets: [['s']] },

View File

@ -157,7 +157,6 @@ class Search extends React.Component {
{ value: 'name', label: i18n('common.name') },
{ value: 'artist', label: i18n('common.artist') },
{ value: 'duration', label: i18n('common.duration') },
{ value: 'uri', label: i18n('common.source') },
];
const provider_options = uri_schemes.map((item) => ({

View File

@ -3,7 +3,6 @@ import { connect, useDispatch } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Routes, Route } from 'react-router-dom';
import PusherConnectionList from '../components/PusherConnectionList';
import SourcesPriority from '../components/Fields/SourcesPriority';
import Commands from '../components/Fields/Commands';
import TextField from '../components/Fields/TextField';
import SelectField from '../components/Fields/SelectField';
@ -333,22 +332,6 @@ class Settings extends React.Component {
</div>
</div>
<div className="field sources-priority">
<div className="name">
<I18n path="settings.interface.sources_priority.label" />
</div>
<div className="input">
<SourcesPriority
uri_schemes={mopidy.uri_schemes ? mopidy.uri_schemes : []}
uri_schemes_priority={ui.uri_schemes_priority ? ui.uri_schemes_priority : []}
uiActions={uiActions}
/>
<div className="description">
<I18n path="settings.interface.sources_priority.description" />
</div>
</div>
</div>
{isHosted() ? null : (
<div className="field checkbox">
<div className="name">

View File

@ -24,7 +24,6 @@
@import 'components/dropdown-field';
@import 'components/autocomplete-field';
@import 'components/filter-field';
@import 'components/sources-priority-field';
@import 'components/sub-views';
@import 'components/sub-tabs';
@import 'components/debug';
@ -36,7 +35,6 @@
@import 'components/commands';
@import 'components/related-artists';
@import 'components/error-message';
@import 'components/sortable';
@import 'components/mute-control';
@import 'components/select-field';
@import 'components/pin-list';

View File

@ -1,6 +0,0 @@
.sortable {
&-ghost {
opacity: 0.15;
}
}

View File

@ -1,40 +0,0 @@
.sources-priority-field {
padding: 7px 0;
.source {
@include animate();
display: inline-block;
border-radius: 3px;
padding: 6px 8px 4px 3px;
margin: 0 5px 5px 0;
cursor: move;
font-size: 12px;
font-weight: normal;
background: rgba(255,255,255,0.1);
color: colour('white');
&.sortable-chosen {
background: colour('grey');
}
.icon {
font-size: 14px;
padding-right: 0.1em;
color: colour('darkest_grey');
}
&:hover {
background: rgba(255,255,255,0.2);
}
@include theme('light') {
background: colour('faint_grey');
color: inherit;
&:hover {
background: darken(colour('faint_grey'), 4%);
}
}
}
}