feat(webui): show Snackbar on connnected/disconnected to a device

This commit is contained in:
Tsiry Sandratraina
2023-01-11 21:40:48 +03:00
parent cf150c09b8
commit 12903fa5a1
16 changed files with 587 additions and 21 deletions

View File

@@ -5,7 +5,7 @@ on:
jobs:
release:
name: release ${{ matrix.target }}
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:

View File

@@ -16,7 +16,7 @@ use music_player_types::types::{self, Connected};
use super::{
connect_to, connect_to_current_device,
objects::device::{App, Device},
objects::device::{App, ConnectedDevice, Device, DisconnectedDevice},
};
#[derive(Default)]
@@ -114,6 +114,8 @@ impl DevicesMutation {
None => return Err(Error::new("No source found")),
}
SimpleBroker::<ConnectedDevice>::publish(device.clone().into());
Ok(types::Device::from(device.clone())
.is_connected(Some(&device.clone()))
.into())
@@ -132,6 +134,7 @@ impl DevicesMutation {
match connected_device.remove("current_device") {
Some(device) => {
io_device.clear_source();
SimpleBroker::<DisconnectedDevice>::publish(device.clone().into());
Ok(Some(device.clone().into()))
}
None => Ok(None),
@@ -162,4 +165,12 @@ impl DevicesSubscription {
});
SimpleBroker::<Device>::subscribe()
}
async fn on_connected(&self, ctx: &Context<'_>) -> impl Stream<Item = ConnectedDevice> {
SimpleBroker::<ConnectedDevice>::subscribe()
}
async fn on_disconnected(&self, ctx: &Context<'_>) -> impl Stream<Item = DisconnectedDevice> {
SimpleBroker::<DisconnectedDevice>::subscribe()
}
}

View File

@@ -64,3 +64,115 @@ impl From<types::Device> for Device {
}
}
}
#[derive(Default, Clone, Serialize)]
pub struct ConnectedDevice {
pub id: ID,
pub name: String,
pub host: String,
pub port: u16,
pub service: String,
pub app: String,
pub is_connected: bool,
}
#[Object]
impl ConnectedDevice {
async fn id(&self) -> &str {
&self.id
}
async fn name(&self) -> &str {
&self.name
}
async fn host(&self) -> &str {
&self.host
}
async fn port(&self) -> u16 {
self.port
}
async fn service(&self) -> &str {
&self.service
}
async fn app(&self) -> &str {
&self.app
}
async fn is_connected(&self) -> bool {
self.is_connected
}
}
impl From<types::Device> for ConnectedDevice {
fn from(device: types::Device) -> Self {
Self {
id: ID::from(device.id),
name: device.name,
host: device.host,
port: device.port,
service: device.service,
app: device.app,
is_connected: true,
}
}
}
#[derive(Default, Clone, Serialize)]
pub struct DisconnectedDevice {
pub id: ID,
pub name: String,
pub host: String,
pub port: u16,
pub service: String,
pub app: String,
pub is_connected: bool,
}
#[Object]
impl DisconnectedDevice {
async fn id(&self) -> &str {
&self.id
}
async fn name(&self) -> &str {
&self.name
}
async fn host(&self) -> &str {
&self.host
}
async fn port(&self) -> u16 {
self.port
}
async fn service(&self) -> &str {
&self.service
}
async fn app(&self) -> &str {
&self.app
}
async fn is_connected(&self) -> bool {
self.is_connected
}
}
impl From<types::Device> for DisconnectedDevice {
fn from(device: types::Device) -> Self {
Self {
id: ID::from(device.id),
name: device.name,
host: device.host,
port: device.port,
service: device.service,
app: device.app,
is_connected: false,
}
}
}

View File

@@ -1,7 +1,7 @@
{
"files": {
"main.css": "/static/css/main.5e35567c.css",
"main.js": "/static/js/main.a5489166.js",
"main.js": "/static/js/main.de307e91.js",
"static/js/787.26bf0a29.chunk.js": "/static/js/787.26bf0a29.chunk.js",
"static/media/RockfordSans-ExtraBold.otf": "/static/media/RockfordSans-ExtraBold.1513e8fd97078bfb7708.otf",
"static/media/RockfordSans-Bold.otf": "/static/media/RockfordSans-Bold.c9f599ae01b13e565598.otf",
@@ -10,11 +10,11 @@
"static/media/RockfordSans-Light.otf": "/static/media/RockfordSans-Light.b4a12e8abb38f7d105c4.otf",
"index.html": "/index.html",
"main.5e35567c.css.map": "/static/css/main.5e35567c.css.map",
"main.a5489166.js.map": "/static/js/main.a5489166.js.map",
"main.de307e91.js.map": "/static/js/main.de307e91.js.map",
"787.26bf0a29.chunk.js.map": "/static/js/787.26bf0a29.chunk.js.map"
},
"entrypoints": [
"static/css/main.5e35567c.css",
"static/js/main.a5489166.js"
"static/js/main.de307e91.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Music Player</title><script defer="defer" src="/static/js/main.a5489166.js"></script><link href="/static/css/main.5e35567c.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Music Player</title><script defer="defer" src="/static/js/main.de307e91.js"></script><link href="/static/css/main.5e35567c.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -377,6 +377,129 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ConnectedDevice",
"description": null,
"fields": [
{
"name": "app",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "host",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "isConnected",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "port",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "service",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "CurrentlyPlayingSong",
@@ -571,6 +694,129 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DisconnectedDevice",
"description": null,
"fields": [
{
"name": "app",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "host",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "isConnected",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "port",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "service",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "Float",
@@ -2891,6 +3137,38 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "onConnected",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ConnectedDevice",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "onDisconnected",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DisconnectedDevice",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "onNewDevice",
"description": null,

View File

@@ -71,6 +71,9 @@ const ConnectModal: FC<ConnectModalProps> = ({
disconnectFromDevice,
}) => {
const _connectToDevice = (id: string) => {
if (currentDevice?.id === id) {
return;
}
connectToDevice(id);
onClose();
};

View File

@@ -20,7 +20,7 @@ const ConnectButton = styled.button<{
connected?: boolean;
}>`
border: none;
background-color: #ab28fc0d;
background-color: #fbf5ff;
height: 32px;
${(props) => (props.connected ? "width: 272px;" : "40px")}
display: flex;
@@ -30,7 +30,6 @@ const ConnectButton = styled.button<{
position: absolute;
bottom: 0;
left: 0;
overflow: hidden;
`;
const ConnectText = styled.span`
@@ -41,6 +40,8 @@ const ConnectText = styled.span`
text-overflow: ellipsis;
margin-left: 10px;
margin-right: 10px;
overflow: hidden;
white-space: nowrap;
`;
export type SidebarProps = {

View File

@@ -13,3 +13,21 @@ export const NEW_DEVICE = gql`
}
}
`;
export const ON_DEVICE_CONNECTED = gql`
subscription OnDeviceConnected {
onConnected {
id
name
}
}
`;
export const ON_DEVICE_DISCONNECTED = gql`
subscription OnDeviceDisconnected {
onDisconnected {
id
name
}
}
`;

View File

@@ -45,6 +45,17 @@ export type Artist = {
website: Scalars['String'];
};
export type ConnectedDevice = {
__typename?: 'ConnectedDevice';
app: Scalars['String'];
host: Scalars['String'];
id: Scalars['String'];
isConnected: Scalars['Boolean'];
name: Scalars['String'];
port: Scalars['Int'];
service: Scalars['String'];
};
export type CurrentlyPlayingSong = {
__typename?: 'CurrentlyPlayingSong';
index: Scalars['Int'];
@@ -64,6 +75,17 @@ export type Device = {
service: Scalars['String'];
};
export type DisconnectedDevice = {
__typename?: 'DisconnectedDevice';
app: Scalars['String'];
host: Scalars['String'];
id: Scalars['String'];
isConnected: Scalars['Boolean'];
name: Scalars['String'];
port: Scalars['Int'];
service: Scalars['String'];
};
export type Folder = {
__typename?: 'Folder';
id: Scalars['String'];
@@ -365,6 +387,8 @@ export type Subscription = {
currentlyPlayingSong: Track;
folder: FolderChanged;
folders: Array<Folder>;
onConnected: ConnectedDevice;
onDisconnected: DisconnectedDevice;
onNewDevice: Device;
playerState: PlayerState;
playlist: PlaylistChanged;
@@ -454,6 +478,16 @@ export type OnNewDeviceSubscriptionVariables = Exact<{ [key: string]: never; }>;
export type OnNewDeviceSubscription = { __typename?: 'Subscription', onNewDevice: { __typename?: 'Device', id: string, app: string, name: string, service: string, host: string, port: number, isConnected: boolean } };
export type OnDeviceConnectedSubscriptionVariables = Exact<{ [key: string]: never; }>;
export type OnDeviceConnectedSubscription = { __typename?: 'Subscription', onConnected: { __typename?: 'ConnectedDevice', id: string, name: string } };
export type OnDeviceDisconnectedSubscriptionVariables = Exact<{ [key: string]: never; }>;
export type OnDeviceDisconnectedSubscription = { __typename?: 'Subscription', onDisconnected: { __typename?: 'DisconnectedDevice', id: string, name: string } };
export type AlbumFragmentFragment = { __typename?: 'Album', id: string, title: string, artist: string, year?: number | null, cover?: string | null };
export type ArtistFragmentFragment = { __typename?: 'Artist', id: string, name: string, picture: string };
@@ -950,6 +984,66 @@ export function useOnNewDeviceSubscription(baseOptions?: Apollo.SubscriptionHook
}
export type OnNewDeviceSubscriptionHookResult = ReturnType<typeof useOnNewDeviceSubscription>;
export type OnNewDeviceSubscriptionResult = Apollo.SubscriptionResult<OnNewDeviceSubscription>;
export const OnDeviceConnectedDocument = gql`
subscription OnDeviceConnected {
onConnected {
id
name
}
}
`;
/**
* __useOnDeviceConnectedSubscription__
*
* To run a query within a React component, call `useOnDeviceConnectedSubscription` and pass it any options that fit your needs.
* When your component renders, `useOnDeviceConnectedSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useOnDeviceConnectedSubscription({
* variables: {
* },
* });
*/
export function useOnDeviceConnectedSubscription(baseOptions?: Apollo.SubscriptionHookOptions<OnDeviceConnectedSubscription, OnDeviceConnectedSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<OnDeviceConnectedSubscription, OnDeviceConnectedSubscriptionVariables>(OnDeviceConnectedDocument, options);
}
export type OnDeviceConnectedSubscriptionHookResult = ReturnType<typeof useOnDeviceConnectedSubscription>;
export type OnDeviceConnectedSubscriptionResult = Apollo.SubscriptionResult<OnDeviceConnectedSubscription>;
export const OnDeviceDisconnectedDocument = gql`
subscription OnDeviceDisconnected {
onDisconnected {
id
name
}
}
`;
/**
* __useOnDeviceDisconnectedSubscription__
*
* To run a query within a React component, call `useOnDeviceDisconnectedSubscription` and pass it any options that fit your needs.
* When your component renders, `useOnDeviceDisconnectedSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useOnDeviceDisconnectedSubscription({
* variables: {
* },
* });
*/
export function useOnDeviceDisconnectedSubscription(baseOptions?: Apollo.SubscriptionHookOptions<OnDeviceDisconnectedSubscription, OnDeviceDisconnectedSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<OnDeviceDisconnectedSubscription, OnDeviceDisconnectedSubscriptionVariables>(OnDeviceDisconnectedDocument, options);
}
export type OnDeviceDisconnectedSubscriptionHookResult = ReturnType<typeof useOnDeviceDisconnectedSubscription>;
export type OnDeviceDisconnectedSubscriptionResult = Apollo.SubscriptionResult<OnDeviceDisconnectedSubscription>;
export const GetAlbumsDocument = gql`
query GetAlbums($offset: Int, $limit: Int) {
albums(offset: $offset, limit: $limit) {

View File

@@ -1,21 +1,32 @@
import { useSnackbar } from "baseui/snackbar";
import _ from "lodash";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Device } from "../Types/Device";
import {
useConnectedDeviceQuery,
useConnectToDeviceMutation,
useDisconnectFromDeviceMutation,
useListDevicesQuery,
useOnDeviceConnectedSubscription,
useOnDeviceDisconnectedSubscription,
useOnNewDeviceSubscription,
} from "./GraphQL";
export const useDevices = () => {
const navigate = useNavigate();
const [currentDevice, setCurrentDevice] =
useState<Device | undefined>(undefined);
const [devices, setDevices] = useState<Device[]>([]);
const { data } = useOnNewDeviceSubscription();
const { data: listDevicesData } = useListDevicesQuery();
const { data: connectedDeviceData } = useConnectedDeviceQuery();
const { data: connectedDeviceData, refetch } = useConnectedDeviceQuery();
const [connectToDevice] = useConnectToDeviceMutation();
const [disconnectFromDevice] = useDisconnectFromDeviceMutation();
const { data: deviceConnectedData } = useOnDeviceConnectedSubscription();
const { data: deviceDisconnectedData } =
useOnDeviceDisconnectedSubscription();
const { enqueue } = useSnackbar();
useEffect(() => {
if (
@@ -54,14 +65,49 @@ export const useDevices = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, listDevicesData]);
const currentDevice: Device | undefined = connectedDeviceData
? {
useEffect(() => {
if (deviceConnectedData) {
enqueue({
message: `Connected to ${deviceConnectedData.onConnected.name}`,
});
refetch()
.then((result) => {
if (result.data?.connectedDevice) {
setCurrentDevice({
id: result.data.connectedDevice.id,
type: result.data.connectedDevice.app,
name: result.data.connectedDevice.name,
isConnected: result.data.connectedDevice.isConnected,
});
}
})
.catch((e) => console.error(e));
navigate(0);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [deviceConnectedData]);
useEffect(() => {
if (deviceDisconnectedData) {
enqueue({
message: `Disconnected from ${deviceDisconnectedData.onDisconnected.name}`,
});
refetch().catch((e) => console.error(e));
setCurrentDevice(undefined);
navigate(0);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [deviceDisconnectedData]);
useEffect(() => {
connectedDeviceData &&
setCurrentDevice({
id: connectedDeviceData.connectedDevice.id,
type: connectedDeviceData.connectedDevice.app,
name: connectedDeviceData.connectedDevice.name,
isConnected: connectedDeviceData.connectedDevice.isConnected,
}
: undefined;
});
}, [connectedDeviceData]);
return { devices, currentDevice, connectToDevice, disconnectFromDevice };
};

View File

@@ -5,6 +5,7 @@ import reportWebVitals from "./reportWebVitals";
import { Provider as StyletronProvider } from "styletron-react";
import { Client as Styletron } from "styletron-engine-atomic";
import { BaseProvider } from "baseui";
import { PLACEMENT, SnackbarProvider } from "baseui/snackbar";
import { theme } from "./Theme";
import {
ApolloClient,
@@ -22,7 +23,7 @@ import { createTauriLink } from "./TauriLink";
let link: ApolloLink;
if (process.env.REACT_APP_NATIVE_WRAPPER === 'tauri') {
if (process.env.REACT_APP_NATIVE_WRAPPER === "tauri") {
link = createTauriLink();
} else {
const uri =
@@ -34,11 +35,11 @@ if (process.env.REACT_APP_NATIVE_WRAPPER === 'tauri') {
const httpLink = createHttpLink({
uri,
});
const wsLink = new WebSocketLink(
new SubscriptionClient(uri.replace("http", "ws"))
);
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
@@ -67,7 +68,9 @@ render(
<ApolloProvider client={client}>
<StyletronProvider value={engine}>
<BaseProvider theme={theme}>
<App />
<SnackbarProvider placement={PLACEMENT.bottom}>
<App />
</SnackbarProvider>
</BaseProvider>
</StyletronProvider>
</ApolloProvider>