Compare commits
3 Commits
main
...
feat-list-
| Author | SHA1 | Date | |
|---|---|---|---|
| c3562afe8e | |||
| 36369fb1f0 | |||
| fa89bd904f |
@ -14,6 +14,7 @@
|
|||||||
href="./src/lib/bootstrap-icons-1.11.3/font/bootstrap-icons.css"
|
href="./src/lib/bootstrap-icons-1.11.3/font/bootstrap-icons.css"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
<script src="./lib/fusejs-7.1.0.min.js"></script>
|
||||||
<script src="./lib/popper.min.js"></script>
|
<script src="./lib/popper.min.js"></script>
|
||||||
<script src="./lib/bootstrap-5.3.3-dist/js/bootstrap.bundle.min.js"></script>
|
<script src="./lib/bootstrap-5.3.3-dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<title>Playback Device Server</title>
|
<title>Playback Device Server</title>
|
||||||
|
|||||||
180
www/src/components/list-manager.jsx
Normal file
180
www/src/components/list-manager.jsx
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import { createEffect, createMemo, createSignal, mergeProps } from "solid-js";
|
||||||
|
|
||||||
|
function ListManager(props) {
|
||||||
|
props = mergeProps(
|
||||||
|
{
|
||||||
|
items: [],
|
||||||
|
availableItems: [],
|
||||||
|
itemToString: () => "",
|
||||||
|
style: "",
|
||||||
|
onItemSelect: () => {},
|
||||||
|
onItemDeselect: () => {},
|
||||||
|
itemsTitle: "Selected items",
|
||||||
|
availableItemsTitle: "Available items",
|
||||||
|
},
|
||||||
|
props
|
||||||
|
);
|
||||||
|
|
||||||
|
const itemToString = (item) => props.itemToString(item);
|
||||||
|
const [selectedAvailableItemIndex, setSelectedAvailableItemIndex] =
|
||||||
|
createSignal(-1);
|
||||||
|
const [selectedItemIndex, setSelectedItemIndex] = createSignal(-1);
|
||||||
|
const [itemsSearchString, setItemsSearchString] = createSignal("");
|
||||||
|
const [availableItemsSearchString, setAvailableItemsSearchString] =
|
||||||
|
createSignal("");
|
||||||
|
|
||||||
|
const itemsFuse = createMemo(
|
||||||
|
() =>
|
||||||
|
new Fuse(props.items, {
|
||||||
|
keys: [{ name: "label", getFn: (item) => props.itemToString(item) }],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const availableItemsFuse = createMemo(
|
||||||
|
() =>
|
||||||
|
new Fuse(props.availableItems, {
|
||||||
|
keys: [{ name: "label", getFn: (item) => props.itemToString(item) }],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
createEffect(() =>
|
||||||
|
console.log(availableItemsFuse().search(availableItemsSearchString()))
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectableAvailableItems = createMemo(() =>
|
||||||
|
(availableItemsSearchString()
|
||||||
|
? availableItemsFuse()
|
||||||
|
.search(availableItemsSearchString())
|
||||||
|
.map((item) => item.item)
|
||||||
|
: props.availableItems
|
||||||
|
).filter((item) => !props.items.includes(item))
|
||||||
|
);
|
||||||
|
const selectableItems = createMemo(() =>
|
||||||
|
itemsSearchString()
|
||||||
|
? itemsFuse().search(itemsSearchString()).map((item) => item.item)
|
||||||
|
: props.items
|
||||||
|
);
|
||||||
|
const canSelect = createMemo(
|
||||||
|
() =>
|
||||||
|
selectedAvailableItemIndex() >= 0 &&
|
||||||
|
selectedAvailableItemIndex() < selectableAvailableItems().length
|
||||||
|
);
|
||||||
|
const canDeselect = createMemo(
|
||||||
|
() =>
|
||||||
|
selectedItemIndex() >= 0 && selectedItemIndex() < selectableItems().length
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleSelectItem() {
|
||||||
|
let itemIndex = selectedAvailableItemIndex();
|
||||||
|
let item = selectableAvailableItems()[itemIndex];
|
||||||
|
if (itemIndex === selectableAvailableItems().length - 1) {
|
||||||
|
setSelectedAvailableItemIndex(itemIndex - 1);
|
||||||
|
}
|
||||||
|
props.onItemSelect(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeselectItem() {
|
||||||
|
let itemIndex = selectedItemIndex();
|
||||||
|
let item = selectableItems()[itemIndex];
|
||||||
|
if (itemIndex === selectableItems().length - 1) {
|
||||||
|
setSelectedItemIndex(itemIndex - 1);
|
||||||
|
}
|
||||||
|
props.onItemDeselect(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListItem(props) {
|
||||||
|
props = mergeProps(
|
||||||
|
{ children: "", selected: false, onClick: () => {} },
|
||||||
|
props
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={"px-2" + (props.selected ? " bg-secondary" : "")}
|
||||||
|
role="button"
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ItemList(props) {
|
||||||
|
props = mergeProps(
|
||||||
|
{
|
||||||
|
items: [],
|
||||||
|
onItemSelected: () => {},
|
||||||
|
selectedItemIndex: -1,
|
||||||
|
onSearchStringChange: () => {},
|
||||||
|
},
|
||||||
|
props
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span
|
||||||
|
class="input-group-text border-bottom-0 rounded-bottom-0"
|
||||||
|
id="basic-addon1"
|
||||||
|
>
|
||||||
|
<i class="bi bi-search"></i>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control form-control-sm border-bottom-0 rounded-bottom-0"
|
||||||
|
onInput={(event) => props.onSearchStringChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="rounded rounded-top-0 border bg-body flex-fill overflow-y-scroll">
|
||||||
|
{props.items.map((item, index) => (
|
||||||
|
<ListItem
|
||||||
|
onClick={() => props.onItemSelected(index)}
|
||||||
|
selected={index === props.selectedItemIndex}
|
||||||
|
>
|
||||||
|
{itemToString(item)}
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={"d-flex"} style={props.style}>
|
||||||
|
<div class="flex-shrink-1 w-50 d-flex flex-column">
|
||||||
|
<div class="px-2">{props.itemsTitle}</div>
|
||||||
|
<ItemList
|
||||||
|
items={selectableItems()}
|
||||||
|
onItemSelected={(index) => setSelectedItemIndex(index)}
|
||||||
|
selectedItemIndex={selectedItemIndex()}
|
||||||
|
onSearchStringChange={(value) => setItemsSearchString(value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="p-2 d-flex flex-column justify-content-center align-items-center">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-outline-secondary mb-2"
|
||||||
|
onClick={handleSelectItem}
|
||||||
|
disabled={!canSelect()}
|
||||||
|
>
|
||||||
|
<i class="bi bi-caret-left-fill"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-outline-secondary"
|
||||||
|
onClick={handleDeselectItem}
|
||||||
|
disabled={!canDeselect()}
|
||||||
|
>
|
||||||
|
<i class="bi bi-caret-right-fill"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-1 w-50 d-flex flex-column">
|
||||||
|
<div class="px-2">{props.availableItemsTitle}</div>
|
||||||
|
<ItemList
|
||||||
|
items={selectableAvailableItems()}
|
||||||
|
onItemSelected={(index) => setSelectedAvailableItemIndex(index)}
|
||||||
|
selectedItemIndex={selectedAvailableItemIndex()}
|
||||||
|
onSearchStringChange={(value) => setAvailableItemsSearchString(value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListManager;
|
||||||
9
www/src/lib/fusejs-7.1.0.min.js
vendored
Normal file
9
www/src/lib/fusejs-7.1.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,9 +1,10 @@
|
|||||||
import { createMemo, createSignal } from "solid-js";
|
import { createMemo, createResource, createSignal } from "solid-js";
|
||||||
import ValidatedTextInput from "../components/validated-text-input.jsx";
|
import ValidatedTextInput from "../components/validated-text-input.jsx";
|
||||||
import RemotesService from "../services/remotes-service.js";
|
import RemotesService from "../services/remotes-service.js";
|
||||||
import EventEmitter from "../tools/event-emitter.js";
|
import EventEmitter from "../tools/event-emitter.js";
|
||||||
import ModalHandler from "./modal-handler.js";
|
import ModalHandler from "./modal-handler.js";
|
||||||
import Modal from "./modal.jsx";
|
import Modal from "./modal.jsx";
|
||||||
|
import ListManager from "../components/list-manager.jsx";
|
||||||
|
|
||||||
const eventEmitter = new EventEmitter();
|
const eventEmitter = new EventEmitter();
|
||||||
const REMOTE_CREATED_EVENT = "success";
|
const REMOTE_CREATED_EVENT = "success";
|
||||||
@ -12,6 +13,7 @@ const MIN_TITLE_LENGTH = 3;
|
|||||||
function CreateRemoteModal(props) {
|
function CreateRemoteModal(props) {
|
||||||
const [title, setTitle] = createSignal("");
|
const [title, setTitle] = createSignal("");
|
||||||
const [commands, setCommands] = createSignal([]);
|
const [commands, setCommands] = createSignal([]);
|
||||||
|
const [availableCommands] = createResource(RemotesService.getCommands);
|
||||||
const [error, setError] = createSignal("");
|
const [error, setError] = createSignal("");
|
||||||
|
|
||||||
const isTitleValid = createMemo(() => title().length >= MIN_TITLE_LENGTH);
|
const isTitleValid = createMemo(() => title().length >= MIN_TITLE_LENGTH);
|
||||||
@ -40,6 +42,16 @@ function CreateRemoteModal(props) {
|
|||||||
setError("");
|
setError("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleCommandSelect(item) {
|
||||||
|
setCommands([...commands(), item]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCommandDeselect(item) {
|
||||||
|
setCommands(
|
||||||
|
commands().filter((command) => command.getId() !== item.getId())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
ref={props.ref}
|
ref={props.ref}
|
||||||
@ -47,7 +59,7 @@ function CreateRemoteModal(props) {
|
|||||||
modalTitle="New Remote"
|
modalTitle="New Remote"
|
||||||
centered={true}
|
centered={true}
|
||||||
>
|
>
|
||||||
<div class="modal-body" style="overflow-y:inherit !important;">
|
<div class="modal-body bg-body-tertiary">
|
||||||
<Show when={error() !== ""}>
|
<Show when={error() !== ""}>
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="alert alert-danger" role="alert">
|
||||||
{error()}
|
{error()}
|
||||||
@ -66,8 +78,19 @@ function CreateRemoteModal(props) {
|
|||||||
errorText={`Title must be at least ${MIN_TITLE_LENGTH} characters long`}
|
errorText={`Title must be at least ${MIN_TITLE_LENGTH} characters long`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<ListManager
|
||||||
|
style="height: 20em;"
|
||||||
|
items={commands()}
|
||||||
|
availableItems={availableCommands()}
|
||||||
|
itemToString={(command) =>
|
||||||
|
`${command.getTitle()} (${command.getProtocol()}:${command.getCommandType()})`
|
||||||
|
}
|
||||||
|
onItemSelect={handleCommandSelect}
|
||||||
|
onItemDeselect={handleCommandDeselect}
|
||||||
|
itemsTitle="Selected Commands"
|
||||||
|
availableItemsTitle="Available Commands"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@ -1,7 +1,33 @@
|
|||||||
|
import Command from "../data/command";
|
||||||
import Serializer from "../data/serializer";
|
import Serializer from "../data/serializer";
|
||||||
|
|
||||||
function RemotesService() {
|
function RemotesService() {
|
||||||
let commands = [];
|
let commands = [
|
||||||
|
new Command({
|
||||||
|
id: 1,
|
||||||
|
protocol: "samsung",
|
||||||
|
commandNumber: 1,
|
||||||
|
device: 7,
|
||||||
|
commandType: "power",
|
||||||
|
title: "Power Samsung",
|
||||||
|
}),
|
||||||
|
new Command({
|
||||||
|
id: 2,
|
||||||
|
protocol: "samsung",
|
||||||
|
commandNumber: 2,
|
||||||
|
device: 7,
|
||||||
|
commandType: "input",
|
||||||
|
title: "Input Samsung",
|
||||||
|
}),
|
||||||
|
new Command({
|
||||||
|
id: 3,
|
||||||
|
protocol: "samsung",
|
||||||
|
commandNumber: 3,
|
||||||
|
device: 7,
|
||||||
|
commandType: "volume_up",
|
||||||
|
title: "Volume Up Samsung",
|
||||||
|
}),
|
||||||
|
];
|
||||||
let remotes = [];
|
let remotes = [];
|
||||||
|
|
||||||
async function getRemotes() {
|
async function getRemotes() {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user