diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index d35079d..0208652 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -1,10 +1,12 @@
name: Playwright Tests
on:
push:
- branches-ignore: [ gh-pages ]
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
concurrency:
group: ${{ github.ref }}
- cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
+ cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
test:
timeout-minutes: 60
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1716a45..756f5d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,11 @@ Shazarr notable changes.
[Keep a Changelog](http://keepachangelog.com/en/1.0.0/) format.
+## 📦 0.3.0
+### 🖍 Changed
+* Results shows now as a inline card instead of a dedicated screen.
+* Enhance UI for landscape mode
+
## 📦 0.2.1
### 🐛 Fixed
* Fix Tidarr network error
diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle
index 6406b00..5f72368 100644
--- a/android/capacitor.settings.gradle
+++ b/android/capacitor.settings.gradle
@@ -1,30 +1,30 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
-project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.3.1_@capacitor+core@8.3.1/node_modules/@capacitor/android/capacitor')
+project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.3.4_@capacitor+core@8.3.4/node_modules/@capacitor/android/capacitor')
include ':capacitor-app'
-project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@8.1.0_@capacitor+core@8.3.1/node_modules/@capacitor/app/android')
+project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@8.1.0_@capacitor+core@8.3.4/node_modules/@capacitor/app/android')
include ':capacitor-device'
-project(':capacitor-device').projectDir = new File('../node_modules/.pnpm/@capacitor+device@8.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/device/android')
+project(':capacitor-device').projectDir = new File('../node_modules/.pnpm/@capacitor+device@8.0.2_@capacitor+core@8.3.4/node_modules/@capacitor/device/android')
include ':capacitor-filesystem'
-project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@8.1.2_@capacitor+core@8.3.1/node_modules/@capacitor/filesystem/android')
+project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@8.1.2_@capacitor+core@8.3.4/node_modules/@capacitor/filesystem/android')
include ':capacitor-haptics'
-project(':capacitor-haptics').projectDir = new File('../node_modules/.pnpm/@capacitor+haptics@8.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/haptics/android')
+project(':capacitor-haptics').projectDir = new File('../node_modules/.pnpm/@capacitor+haptics@8.0.2_@capacitor+core@8.3.4/node_modules/@capacitor/haptics/android')
include ':capacitor-local-notifications'
-project(':capacitor-local-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+local-notifications@8.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/local-notifications/android')
+project(':capacitor-local-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+local-notifications@8.1.0_@capacitor+core@8.3.4/node_modules/@capacitor/local-notifications/android')
include ':capacitor-network'
-project(':capacitor-network').projectDir = new File('../node_modules/.pnpm/@capacitor+network@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/network/android')
+project(':capacitor-network').projectDir = new File('../node_modules/.pnpm/@capacitor+network@8.0.1_@capacitor+core@8.3.4/node_modules/@capacitor/network/android')
include ':capacitor-preferences'
-project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/preferences/android')
+project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@8.0.1_@capacitor+core@8.3.4/node_modules/@capacitor/preferences/android')
include ':capacitor-screen-orientation'
-project(':capacitor-screen-orientation').projectDir = new File('../node_modules/.pnpm/@capacitor+screen-orientation@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/screen-orientation/android')
+project(':capacitor-screen-orientation').projectDir = new File('../node_modules/.pnpm/@capacitor+screen-orientation@8.0.1_@capacitor+core@8.3.4/node_modules/@capacitor/screen-orientation/android')
include ':capacitor-voice-recorder'
-project(':capacitor-voice-recorder').projectDir = new File('../node_modules/.pnpm/capacitor-voice-recorder@7.0.6_@capacitor+core@8.3.1_@capacitor+filesystem@8.1.2_@capacitor+core@8.3.1_/node_modules/capacitor-voice-recorder/android')
+project(':capacitor-voice-recorder').projectDir = new File('../node_modules/.pnpm/capacitor-voice-recorder@7.0.6_@capacitor+core@8.3.4_@capacitor+filesystem@8.1.2_@capacitor+core@8.3.4_/node_modules/capacitor-voice-recorder/android')
diff --git a/dist/index.html b/dist/index.html
index 1116db2..2255603 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -24,7 +24,7 @@
})();
-
+
diff --git a/package.json b/package.json
index b150e86..22c0be3 100644
--- a/package.json
+++ b/package.json
@@ -66,11 +66,11 @@
"quality:depcheck": "npx depcheck",
"quality:eslint": "eslint .",
"quality:eslint-fix": "eslint --fix .",
- "testing:run": "docker run --rm -it -v .:/srv -w /srv mcr.microsoft.com/playwright:v1.59.1-noble sh -c 'npm i -g pnpm && pnpm install && npx playwright test'",
+ "testing:run": "docker run --rm -it -v .:/srv -w /srv mcr.microsoft.com/playwright:v1.60.0-noble sh -c 'npm i -g pnpm@10.28.2 && pnpm install --no-frozen-lockfile && npx playwright test'",
"testing:ui": "npx playwright test --ui",
- "testing:ci": "docker run --rm -e CI=true -v .:/srv -w /srv mcr.microsoft.com/playwright:v1.59.1-noble sh -c 'npm i -g pnpm && pnpm install && npx playwright test'",
- "testing:show-report": "docker run --rm -it -p 9323:9323 -v .:/srv -w /srv mcr.microsoft.com/playwright:v1.59.1-noble npx playwright show-report ./testing/playwright-report --host 0.0.0.0",
- "testing:update-snapshots": "docker run --rm -it -v .:/srv -w /srv mcr.microsoft.com/playwright:v1.59.1-noble sh -c 'npm i -g pnpm && pnpm install && npx playwright test --update-snapshots'"
+ "testing:ci": "docker run --rm -e CI=true -v .:/srv -w /srv mcr.microsoft.com/playwright:v1.60.0-noble sh -c 'npm i -g pnpm@10.28.2 && pnpm install --no-frozen-lockfile && npx playwright test'",
+ "testing:show-report": "docker run --rm -it -p 9323:9323 -v .:/srv -w /srv mcr.microsoft.com/playwright:v1.60.0-noble npx playwright show-report ./testing/playwright-report --host 0.0.0.0",
+ "testing:update-snapshots": "docker run --rm -it -v .:/srv -w /srv mcr.microsoft.com/playwright:v1.60.0-noble sh -c 'npm i -g pnpm@10.28.2 && pnpm install --no-frozen-lockfile && npx playwright test --update-snapshots'"
},
"pnpm": {
"overrides": {
diff --git a/src/App.tsx b/src/App.tsx
index 60157eb..7c8dcf6 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -11,9 +11,9 @@ import {
Typography,
} from "@mui/material";
-import Config from "./components/Config";
+import { AlertHandler } from "./components/Common/Alert/AlertHandler";
+import BottomActions from "./components/Common/BottomActions";
import { ConfigProvider } from "./components/Config/Provider";
-import HistoryList from "./components/History/List";
import { HistoryProvider } from "./components/History/Provider";
import ShazarrButton from "./components/Shazarr/Button";
import { ShazarrProvider } from "./components/Shazarr/Provider";
@@ -43,7 +43,7 @@ function App() {
-
+
Shazarr
@@ -63,12 +63,10 @@ function App() {
>
-
-
-
-
+
+
diff --git a/src/components/Config/Alert.tsx b/src/components/Common/Alert/Alert.tsx
similarity index 87%
rename from src/components/Config/Alert.tsx
rename to src/components/Common/Alert/Alert.tsx
index 5f9af1a..4ff2c08 100644
--- a/src/components/Config/Alert.tsx
+++ b/src/components/Common/Alert/Alert.tsx
@@ -1,6 +1,6 @@
import { Alert, Box } from "@mui/material";
-import errorCode, { ErrorCodeType } from "./errorCode";
+import errorCode, { ErrorCodeType } from "../../Config/errorCode";
export default function ApiErrorAlert({
message,
diff --git a/src/components/Common/Alert/AlertHandler.tsx b/src/components/Common/Alert/AlertHandler.tsx
new file mode 100644
index 0000000..1bb8a7c
--- /dev/null
+++ b/src/components/Common/Alert/AlertHandler.tsx
@@ -0,0 +1,51 @@
+import { useState } from "react";
+import { Box } from "@mui/material";
+
+import { useConfigProvider } from "../../Config/useConfig";
+import { useHistoryProvider } from "../../History/useHistory";
+import ShazarrResults from "../../Shazarr/Result";
+import InlineResultCard from "../../Shazarr/ui/InlineResultCard";
+import { useShazarrProvider } from "../../Shazarr/useShazarr";
+
+import ApiErrorAlert from "./Alert";
+
+export const AlertHandler = () => {
+ const [resultOpen, setResultOpen] = useState(false);
+ const { recordingStatus, recordingError, showInlineResult, actions: { dismissInlineResult } } = useShazarrProvider();
+ const { isNetworkConnected } = useConfigProvider();
+ const { history } = useHistoryProvider();
+
+ const lastItem = history?.[history.length - 1];
+
+ function handleDismiss() {
+ dismissInlineResult();
+ setResultOpen(false);
+ }
+
+ function handleOpenResult() {
+ dismissInlineResult();
+ setResultOpen(true);
+ }
+
+ return (
+
+ setResultOpen(false)}
+ />
+ {showInlineResult && lastItem?.data && recordingStatus === "inactive" && (
+
+ )}
+ {!isNetworkConnected && recordingStatus === "inactive" && (
+
+ )}
+ {recordingError && recordingStatus === "inactive" && (
+
+ )}
+
+ );
+};
diff --git a/src/components/Common/BottomActions.tsx b/src/components/Common/BottomActions.tsx
new file mode 100644
index 0000000..e358894
--- /dev/null
+++ b/src/components/Common/BottomActions.tsx
@@ -0,0 +1,76 @@
+import { useState } from "react";
+import styled from "@emotion/styled";
+import { List, Settings } from "@mui/icons-material";
+import { Box, Fab, useMediaQuery } from "@mui/material";
+
+import Config from "../Config";
+import { useConfigProvider } from "../Config/useConfig";
+import HistoryList from "../History/List";
+import { useHistoryProvider } from "../History/useHistory";
+import { useShazarrProvider } from "../Shazarr/useShazarr";
+
+export default function BottomActions() {
+ const [configOpen, setConfigOpen] = useState(false);
+ const [historyOpen, setHistoryOpen] = useState(false);
+ const { recordingStatus } = useShazarrProvider();
+ const { formConfig } = useConfigProvider();
+ const { history } = useHistoryProvider();
+ const isLandscape = useMediaQuery("(orientation: landscape)");
+
+ const disabled = recordingStatus !== "inactive";
+
+ return (
+ <>
+ setConfigOpen(false)} />
+ setHistoryOpen(false)} />
+
+
+ {formConfig && (
+ setConfigOpen(true)}
+ disabled={disabled}
+ aria-label="Configuration"
+ >
+
+
+ )}
+ {history && history.length > 0 && (
+ setHistoryOpen(true)}
+ disabled={disabled}
+ aria-label="Records"
+ >
+
+
+ )}
+
+ >
+ );
+}
+
+const ActionsContainer = styled(Box)<{ landscape: boolean }>`
+ position: fixed;
+ z-index: 1000;
+ display: flex;
+ gap: 24px;
+
+ ${({ landscape }) =>
+ landscape
+ ? `
+ right: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ flex-direction: column;
+ `
+ : `
+ bottom: 40px;
+ left: 0;
+ right: 0;
+ justify-content: center;
+ flex-direction: row;
+ `}
+`;
diff --git a/src/components/Config/Form.tsx b/src/components/Config/Form.tsx
deleted file mode 100644
index 07c973c..0000000
--- a/src/components/Config/Form.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import { FormEvent, useState } from "react";
-import styled from "@emotion/styled";
-import {
- Box,
- Button,
- Dialog,
- DialogContent,
- Input,
- Link,
- Paper,
-} from "@mui/material";
-
-import { useShazarrProvider } from "../Shazarr/useShazarr";
-
-import { useConfigProvider } from "./useConfig";
-
-export default function ConfigForm() {
- const [showApiForm, setShowApiForm] = useState();
- const { recordingStatus } = useShazarrProvider();
- const {
- config,
- formConfig,
- actions: { setConfig },
- } = useConfigProvider();
-
- if (!formConfig) return null;
-
- function saveConfig(e: FormEvent) {
- e.preventDefault();
- const data = new FormData(e.currentTarget);
- setConfig({
- ...config,
- lidarr_url: data.get("lidarr_url")?.toString() || "",
- lidarr_api_key: data.get("lidarr_api_key")?.toString() || "",
- tidarr_url: data.get("tidarr_url")?.toString() || "",
- tidarr_api_key: data.get("tidarr_api_key")?.toString() || "",
- custom_service_url: data.get("custom_service_url")?.toString() || "",
- custom_service_name: data.get("custom_service_name")?.toString() || "",
- });
- setShowApiForm(false);
- }
-
- return (
- <>
-
-
-
-
- >
- );
-}
-
-const ButtonConfig = styled.div`
- margin-bottom: 1rem;
-`;
diff --git a/src/components/Config/index.tsx b/src/components/Config/index.tsx
index ec91cec..b6a144a 100644
--- a/src/components/Config/index.tsx
+++ b/src/components/Config/index.tsx
@@ -1,25 +1,102 @@
+import { FormEvent } from "react";
+import {
+ Box,
+ Button,
+ Drawer,
+ Input,
+ Link,
+ Paper,
+} from "@mui/material";
+
import { useShazarrProvider } from "../Shazarr/useShazarr";
-import ApiErrorAlert from "./Alert";
-import ConfigForm from "./Form";
import { useConfigProvider } from "./useConfig";
-export default function Config() {
- const { recordingStatus, recordingError } = useShazarrProvider();
- const { formConfig, isNetworkConnected } = useConfigProvider();
+interface Props {
+ open: boolean;
+ onClose: () => void;
+}
+
+export default function Config({ open, onClose }: Props) {
+ const { recordingStatus } = useShazarrProvider();
+ const {
+ config,
+ formConfig,
+ actions: { setConfig },
+ } = useConfigProvider();
if (!formConfig) return null;
- return (
- <>
- {!isNetworkConnected && recordingStatus === "inactive" && (
-
- )}
- {recordingError && recordingStatus === "inactive" && (
-
- )}
+ function saveConfig(e: FormEvent) {
+ e.preventDefault();
+ const data = new FormData(e.currentTarget);
+ setConfig({
+ ...config,
+ lidarr_url: data.get("lidarr_url")?.toString() || "",
+ lidarr_api_key: data.get("lidarr_api_key")?.toString() || "",
+ tidarr_url: data.get("tidarr_url")?.toString() || "",
+ tidarr_api_key: data.get("tidarr_api_key")?.toString() || "",
+ custom_service_url: data.get("custom_service_url")?.toString() || "",
+ custom_service_name: data.get("custom_service_name")?.toString() || "",
+ });
+ onClose();
+ }
-
- >
+ return (
+
+ saveConfig(e)}
+ sx={{
+ display: "flex",
+ alignSelf: "center",
+ flexDirection: "column",
+ padding: 2,
+ maxWidth: 480,
+ width: "100%",
+ }}>
+ {Object.entries(formConfig)?.map((field, index) => (
+
+
+
+
+
+ ))}
+
+
+ Github page -{" "}
+ {import.meta.env.VITE_STAGE === "testing" || import.meta.env.MODE === "development"
+ ? "v0.0.0"
+ : import.meta.env.VITE_CURRENT_VERSION}
+
+
+
+
+
+
+
);
}
diff --git a/src/components/History/Card.tsx b/src/components/History/Card.tsx
index 0d8cc09..2333fd2 100644
--- a/src/components/History/Card.tsx
+++ b/src/components/History/Card.tsx
@@ -1,3 +1,4 @@
+import { useState } from "react";
import { DeleteForever, RemoveRedEye, Search } from "@mui/icons-material";
import { ButtonBase, CardActions, CardMedia, IconButton } from "@mui/material";
import Card from "@mui/material/Card";
@@ -5,6 +6,7 @@ import CardContent from "@mui/material/CardContent";
import Typography from "@mui/material/Typography";
import { ImageWithFallback } from "../Common/ImageWithFallback";
+import ShazarrResults from "../Shazarr/Result";
import { useShazarrProvider } from "../Shazarr/useShazarr";
import { HistoryItem } from "./context";
@@ -17,8 +19,9 @@ export default function HistoryCard({
item: HistoryItem;
onClose: () => void;
}) {
+ const [resultOpen, setResultOpen] = useState(false);
const {
- actions: { setShazarrResponse, searchOfflineRecord },
+ actions: { searchOfflineRecord },
} = useShazarrProvider();
const {
actions: { deleteHistoryItem },
@@ -29,7 +32,7 @@ export default function HistoryCard({
function handleClickItem() {
if (item?.data) {
- setShazarrResponse(item.data);
+ setResultOpen(true);
} else {
searchOfflineRecord(item);
}
@@ -37,43 +40,45 @@ export default function HistoryCard({
}
return (
-
-
-
-
-
-
-
-
-
- {item.title} {`- ${item.artist}`}
-
-
- {dateRecord}
-
-
-
-
-
-
- {item?.data ? : }
-
- {
- deleteHistoryItem(item.date);
- }}
- >
-
-
-
-
+ <>
+ setResultOpen(false)}
+ />
+
+
+
+
+
+
+
+
+
+ {item.title} {`- ${item.artist}`}
+
+
+ {dateRecord}
+
+
+
+
+
+
+ {item?.data ? : }
+
+ deleteHistoryItem(item.date)}>
+
+
+
+
+ >
);
}
diff --git a/src/components/History/List.tsx b/src/components/History/List.tsx
index ccac935..69eaaf9 100644
--- a/src/components/History/List.tsx
+++ b/src/components/History/List.tsx
@@ -1,63 +1,53 @@
-import { useState } from "react";
-import { Close, List } from "@mui/icons-material";
-import { Box, Button, Container, Drawer, useTheme } from "@mui/material";
-
-import { useShazarrProvider } from "../Shazarr/useShazarr";
+import { Close } from "@mui/icons-material";
+import { Box, Button, Container, Drawer, useMediaQuery, useTheme } from "@mui/material";
import HistoryCard from "./Card";
import { useHistoryProvider } from "./useHistory";
-export default function HistoryList() {
- const { recordingStatus } = useShazarrProvider();
+interface Props {
+ open: boolean;
+ onClose: () => void;
+}
+
+export default function HistoryList({ open, onClose }: Props) {
+ const isLandscape = useMediaQuery("(orientation: landscape)");
const { history } = useHistoryProvider();
- const [listOpen, setListOpen] = useState();
const { palette } = useTheme();
if (!history || history?.length === 0) return null;
return (
-
- }
- variant="outlined"
- fullWidth
- onClick={() => setListOpen(true)}
- disabled={recordingStatus !== "inactive"}
+
+
- Show records ({history?.length})
-
- setListOpen(false)}
- ModalProps={{
- keepMounted: true,
- }}
- >
-
-
- {history?.map((item, index) => (
- setListOpen(false)}
- />
- ))}
- }
- variant="outlined"
- fullWidth
- onClick={() => setListOpen(false)}
- sx={{ marginBottom: 1 }}
- >
- Close records
-
-
-
-
-
+
+ {history?.map((item, index) => (
+
+ ))}
+ }
+ variant="outlined"
+ fullWidth
+ onClick={onClose}
+ sx={{ marginBottom: 1 }}
+ >
+ Close records
+
+
+
+
);
}
diff --git a/src/components/Shazarr/Button.tsx b/src/components/Shazarr/Button.tsx
index ad42591..4fc731e 100644
--- a/src/components/Shazarr/Button.tsx
+++ b/src/components/Shazarr/Button.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import styled from "@emotion/styled";
import { MoreHorizOutlined } from "@mui/icons-material";
import GraphicEqIcon from "@mui/icons-material/GraphicEq";
@@ -16,56 +15,53 @@ export default function ShazarrButton() {
shazarrLoading,
shazarrResponse,
recordingStatus,
- actions: { setRecordingStatus, resetSearch },
+ actions: { startRecording, resetSearch },
} = useShazarrProvider();
return (
-
+
{recordingStatus !== "inactive" ? (
) : null}
- {!shazarrResponse ? (
- <>
-
- {
- resetSearch();
- if (recordingStatus === "inactive") {
- setRecordingStatus("start");
- }
- }}
- >
-
- {recordingStatus === "searching" ? (
-
- ) : recordingStatus === "recording" ? (
-
- ) : (
- <>
-
- >
- )}
-
-
-
- >
- ) : (
-
- )}
+
+
+ {
+ resetSearch();
+ if (recordingStatus === "inactive") {
+ startRecording();
+ }
+ }}
+ >
+
+ {recordingStatus === "searching" ? (
+
+ ) : recordingStatus === "recording" ? (
+
+ ) : (
+ <>
+
+ >
+ )}
+
+
+
+ {shazarrResponse && }
);
}
diff --git a/src/components/Shazarr/Provider.tsx b/src/components/Shazarr/Provider.tsx
index 8e0eb1f..17378a8 100644
--- a/src/components/Shazarr/Provider.tsx
+++ b/src/components/Shazarr/Provider.tsx
@@ -1,12 +1,14 @@
-import { ReactNode, useCallback, useEffect, useState } from "react";
+import { ReactNode, useState } from "react";
import { Haptics } from "@capacitor/haptics";
import { ScreenOrientation } from "@capacitor/screen-orientation";
import { VoiceRecorder } from "capacitor-voice-recorder";
import { Shazam } from "shazam-api";
-import { ShazamRoot, ShazamTrack } from "shazam-api/dist/types";
+import { ShazamRoot } from "shazam-api/dist/types";
import { RECORD_DURATION } from "../../constant";
+import { ErrorCodeType } from "../Config/errorCode";
import { useConfigProvider } from "../Config/useConfig";
+import { HistoryItem } from "../History/context";
import { useHistoryProvider } from "../History/useHistory";
import { mockRecordBase64 } from "./mock/recordBase64";
@@ -17,21 +19,16 @@ export type { RecordingStatusType } from "./context";
export function ShazarrProvider({ children }: { children: ReactNode }) {
const [audio, setAudio] = useState();
- const [shazarrResponse, setShazarrResponse] = useState();
+ const [showInlineResult, setShowInlineResult] = useState(false);
const [shazarrLoading, setShazarrLoading] = useState(false);
- const [recordingError, setRecordingError] = useState<
- ReturnType["isNetworkConnected"] extends boolean
- ? never
- : never
- >();
+ const [recordingError, setRecordingError] = useState();
const [recordingStatus, setRecordingStatus] =
useState("inactive");
- const [historySearch, setHistorySearch] = useState();
- const [cleanHistorySearch, setCleanHistorySearch] = useState();
const { isNetworkConnected } = useConfigProvider();
+
const {
- actions: { addItemToHistory, deleteHistoryItem },
+ actions: { addItemToHistory },
} = useHistoryProvider();
const vibrateAction = async () => {
@@ -42,12 +39,15 @@ export function ShazarrProvider({ children }: { children: ReactNode }) {
}
};
- const resetSearch = useCallback(async () => {
- setShazarrResponse(undefined);
+ const dismissInlineResult = () => {
+ setShowInlineResult(false);
+ };
+
+ const resetSearch = async () => {
+ setShowInlineResult(false);
setAudio(undefined);
setRecordingStatus("inactive");
setShazarrLoading(false);
- setHistorySearch(undefined);
await ScreenOrientation.unlock();
@@ -59,185 +59,146 @@ export function ShazarrProvider({ children }: { children: ReactNode }) {
} catch (e) {
console.error(e);
}
- }, []);
+ };
- const processRecording = useCallback(
- async (duration: number = RECORD_DURATION) => {
- setRecordingError(undefined);
- try {
- const { value: hasPerm } =
- await VoiceRecorder.hasAudioRecordingPermission();
- if (!hasPerm) {
- const { value: resultRequestPerm } =
- await VoiceRecorder.requestAudioRecordingPermission();
+ function handleTesting(date: number) {
+ const audioData = mockRecordBase64;
+ setAudio(audioData);
+ if (!isNetworkConnected) {
+ addItemToHistory({
+ title: "Offline record",
+ artist: "Not discovered",
+ date: date,
+ stream: audioData,
+ });
+ setRecordingStatus("inactive");
+ return;
+ }
+ setRecordingStatus("searching");
+ processTranscoding(audioData);
+ }
- if (!resultRequestPerm) return;
+ const processShazam = async (samples: number[]) => {
+ const shazam = new Shazam();
+
+ shazam
+ .fullRecognizeSong(samples)
+ .then((value: ShazamRoot | null) => {
+ console.log("shazarr:", value);
+ const response = value?.track;
+ if (response?.title) {
+ addItemToHistory({
+ title: response.title,
+ artist: response.subtitle,
+ date: Date.now(),
+ data: response,
+ });
+ setShowInlineResult(true);
+ } else {
+ setRecordingError("SHAZARR_NOT_FOUND" as never);
}
+ })
+ .catch((err) => {
+ console.error("shazarr:", err);
+ setRecordingError("SHAZAM_API_ERROR" as never);
+ })
+ .finally(async () => {
+ await vibrateAction();
+ await ScreenOrientation.unlock();
+ setRecordingStatus("inactive");
+ setShazarrLoading(false);
+ });
+ };
- setRecordingStatus("recording");
- vibrateAction();
- const { value: isRecording } = await VoiceRecorder.startRecording();
+ const processTranscoding = (recordDataBase64: string) => {
+ setShazarrLoading(true);
+ transcodePCM16(recordDataBase64).then((pcm16) => {
+ processShazam(pcm16);
+ });
+ };
- if (isRecording) {
- setTimeout(async () => {
- const { status } = await VoiceRecorder.getCurrentStatus();
- if (recordingStatus !== "inactive" && status === "RECORDING") {
- const {
- value: { recordDataBase64 },
- } = await VoiceRecorder.stopRecording();
+ const startRecording = async (duration: number = RECORD_DURATION) => {
+ setRecordingError(undefined);
+ try {
+ const { value: hasPerm } =
+ await VoiceRecorder.hasAudioRecordingPermission();
- if (import.meta.env.VITE_STAGE === "testing") {
- setAudio(mockRecordBase64);
- } else {
- setAudio(recordDataBase64);
- }
+ if (!hasPerm) {
+ const { value: resultRequestPerm } =
+ await VoiceRecorder.requestAudioRecordingPermission();
- setRecordingStatus("searching");
- await vibrateAction();
- }
- }, duration);
- }
- } catch (err) {
- console.error(err);
- if (import.meta.env.VITE_STAGE === "testing") {
- setAudio(mockRecordBase64);
- setRecordingStatus("searching");
- return;
- }
- resetSearch();
- setRecordingError("ERROR_RECORDING" as never);
+ if (!resultRequestPerm) return;
}
- },
- [recordingStatus, resetSearch],
- );
- const processShazam = useCallback(
- async (samples: number[]) => {
- const shazam = new Shazam();
+ vibrateAction();
+ setRecordingStatus("recording");
- shazam
- .fullRecognizeSong(samples)
- .then((value: ShazamRoot | null) => {
- const response = value?.track;
- if (response?.title && recordingStatus !== "inactive") {
- setShazarrResponse(response);
+ const { value: isRecording } = await VoiceRecorder.startRecording();
- addItemToHistory({
- title: response.title,
- artist: response.subtitle,
- date: Date.now(),
- data: response,
- });
+ if (isRecording) {
+ setTimeout(async () => {
+ const {
+ value: { recordDataBase64 },
+ } = await VoiceRecorder.stopRecording();
- if (historySearch) {
- setCleanHistorySearch(historySearch);
- }
- } else {
- setShazarrResponse(undefined);
- setRecordingError("SHAZARR_NOT_FOUND" as never);
- }
- })
- .catch((err) => {
- console.error(err);
- if (!historySearch) {
+ const audioData = import.meta.env.VITE_STAGE === "testing"
+ ? mockRecordBase64
+ : recordDataBase64;
+
+ if (!audioData) return;
+
+ setAudio(audioData);
+ setRecordingStatus("searching");
+ await vibrateAction();
+
+ if (!isNetworkConnected) {
addItemToHistory({
title: "Offline record",
artist: "Not discovered",
date: Date.now(),
- stream: audio,
+ stream: audioData,
});
+ setRecordingStatus("inactive");
+ return;
}
- setRecordingError("SHAZAM_API_ERROR" as never);
- })
- .finally(async () => {
- await vibrateAction();
- await ScreenOrientation.unlock();
- setRecordingStatus("inactive");
- setShazarrLoading(false);
- });
- },
- [addItemToHistory, audio, historySearch, recordingStatus],
- );
- const processTranscoding = useCallback(() => {
- setShazarrLoading(true);
+ processTranscoding(audioData);
+
+ }, duration);
+ }
+ } catch (err) {
+ console.error(err);
- if (!audio || audio.length === 0) return;
+ if (import.meta.env.VITE_STAGE === "testing") {
+ handleTesting(Date.now());
+ return;
+ };
- if (!isNetworkConnected && !historySearch) {
- addItemToHistory({
- title: "Offline record",
- artist: "Not discovered",
- date: Date.now(),
- stream: audio,
- });
- vibrateAction();
- setRecordingStatus("inactive");
- setShazarrLoading(false);
- } else {
- transcodePCM16(audio).then((pcm16) => {
- if (recordingStatus !== "inactive") {
- processShazam(pcm16);
- }
- });
- }
- }, [
- addItemToHistory,
- audio,
- historySearch,
- isNetworkConnected,
- processShazam,
- recordingStatus,
- ]);
-
- const searchOfflineRecord = (item: Parameters[0]) => {
- if (item?.stream) {
resetSearch();
- setAudio(item.stream);
- setHistorySearch(item.date);
- setRecordingStatus("searching");
- }
+ setRecordingError("ERROR_RECORDING" as never);
+ };
};
- useEffect(() => {
- if (shazarrResponse) return;
-
- switch (recordingStatus) {
- case "start":
- // eslint-disable-next-line react-hooks/set-state-in-effect
- processRecording();
- break;
- case "searching":
- if (audio) {
- processTranscoding();
- }
- break;
- }
- }, [
- audio,
- processRecording,
- processTranscoding,
- recordingStatus,
- shazarrResponse,
- ]);
-
- useEffect(() => {
- if (cleanHistorySearch) {
- deleteHistoryItem(cleanHistorySearch);
- }
- }, [cleanHistorySearch, deleteHistoryItem]);
+ const searchOfflineRecord = (item: HistoryItem) => {
+ if (!item?.stream) return;
+
+ resetSearch();
+ setAudio(item.stream);
+ setRecordingStatus("searching");
+ processTranscoding(item.stream);
+ };
const value = {
recordingError,
shazarrLoading,
- shazarrResponse,
+ showInlineResult,
recordingStatus,
audio,
actions: {
- setRecordingStatus,
+ startRecording,
resetSearch,
- setShazarrResponse,
+ dismissInlineResult,
searchOfflineRecord,
},
};
diff --git a/src/components/Shazarr/Result.tsx b/src/components/Shazarr/Result.tsx
index 1e2be81..65e4695 100644
--- a/src/components/Shazarr/Result.tsx
+++ b/src/components/Shazarr/Result.tsx
@@ -1,91 +1,123 @@
+import { useMemo } from "react";
import { Close } from "@mui/icons-material";
-import { Box, Button, Divider, Stack } from "@mui/material";
-import { ShazamProvider } from "shazam-api/dist/types";
+import { Box, Button, Divider, Drawer, Stack, Table, TableBody, TableCell, TableContainer, TableRow, useMediaQuery } from "@mui/material";
+import { ShazamProvider, ShazamTrack } from "shazam-api/dist/types";
import { useConfigProvider } from "../Config/useConfig";
+import ButtonLyrics from "./ui/ButtonLyrics";
import CardResult from "./ui/Card";
import CustomServiceButton from "./ui/CustomServiceButton";
import LidarrButton from "./ui/LidarrButton";
import StreamProviderButton from "./ui/StreamProviderButton";
import TidarrButton from "./ui/TidarrButton";
-import { useShazarrProvider } from "./useShazarr";
-export default function ShazarrResults() {
- const {
- shazarrResponse,
- actions: { resetSearch },
- } = useShazarrProvider();
+interface Props {
+ data: ShazamTrack | undefined;
+ onClose: () => void;
+}
+export default function ShazarrResults({ data, onClose }: Props) {
+ const isLandscape = useMediaQuery("(orientation: landscape)");
const { config } = useConfigProvider();
- const albumName = shazarrResponse?.sections?.[0]?.metadata?.filter(
+ const albumName = data?.sections?.[0]?.metadata?.filter(
(m: { title: string }) => m?.title === "Album",
)?.[0]?.text;
- if (!shazarrResponse) return null;
+ const lyrics = useMemo(
+ () => data?.sections?.filter((section) => section.type === "LYRICS")?.[0]?.text,
+ [data],
+ );
+
+ if (!data) return null;
return (
- <>
-
-
-
- {config?.lidarr_url && (
-
- )}
- {config?.tidarr_url && (
-
- )}
- {config?.custom_service_url && config?.custom_service_name && (
-
- )}
-
-
-
- {shazarrResponse?.hub?.providers?.map(
- (provider: ShazamProvider, index: number) => (
-
- ),
- )}
-
- {shazarrResponse?.myshazam?.apple?.actions?.[0]?.uri && (
-
- )}
+
+
+
+
+
+
+
+
+
+ {data?.sections?.[0]?.metadata?.map((row, index) => (
+
+
+ {row.title}
+
+ {row.text}
+
+ ))}
+
+
+
-
+
-
-
-
- >
+ {config?.lidarr_url && (
+
+ )}
+ {config?.tidarr_url && (
+
+ )}
+ {config?.custom_service_url && config?.custom_service_name && (
+
+ )}
+
+
+
+ {data?.hub?.providers?.map(
+ (provider: ShazamProvider, index: number) => (
+
+ ),
+ )}
+ {data?.myshazam?.apple?.actions?.[0]?.uri && (
+
+ )}
+
+
+
+
+ }
+ >
+ Close
+
+
+
+
+
);
}
diff --git a/src/components/Shazarr/context.ts b/src/components/Shazarr/context.ts
index b7544d7..253b6f1 100644
--- a/src/components/Shazarr/context.ts
+++ b/src/components/Shazarr/context.ts
@@ -1,26 +1,21 @@
import { createContext } from "react";
-import { ShazamTrack } from "shazam-api/dist/types";
import { ErrorCodeType } from "../Config/errorCode";
import { HistoryItem } from "../History/context";
-export type RecordingStatusType =
- | "start"
- | "recording"
- | "searching"
- | "inactive";
+export type RecordingStatusType = "recording" | "searching" | "inactive";
type ShazarrContextType = {
recordingError: ErrorCodeType | undefined;
shazarrLoading: boolean;
- shazarrResponse: ShazamTrack | undefined;
+ showInlineResult: boolean;
recordingStatus: RecordingStatusType;
audio: string | undefined;
actions: {
- setRecordingStatus: (status: RecordingStatusType) => void;
- setShazarrResponse: (data: ShazamTrack) => void;
+ startRecording: () => void;
searchOfflineRecord: (item: HistoryItem) => void;
resetSearch: () => void;
+ dismissInlineResult: () => void;
};
};
diff --git a/src/components/Shazarr/ui/ButtonLyrics.tsx b/src/components/Shazarr/ui/ButtonLyrics.tsx
new file mode 100644
index 0000000..4382ca6
--- /dev/null
+++ b/src/components/Shazarr/ui/ButtonLyrics.tsx
@@ -0,0 +1,29 @@
+import { ExpandMore } from "@mui/icons-material";
+import { Accordion, AccordionDetails, AccordionSummary, Box, Typography } from "@mui/material";
+
+export default function ButtonLyrics({ lyrics }: { lyrics: string[] | undefined }) {
+ if (!lyrics || lyrics?.length === 0) return null;
+ return (
+
+
+ }
+ aria-controls="panel1a-content"
+ id="panel1a-header"
+ >
+ LYRICS
+
+
+
+ {lyrics?.map((lyric, index) => (
+
+ {lyric}
+
+
+ ))}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/Shazarr/ui/Card.tsx b/src/components/Shazarr/ui/Card.tsx
index e99f2a5..08ae146 100644
--- a/src/components/Shazarr/ui/Card.tsx
+++ b/src/components/Shazarr/ui/Card.tsx
@@ -1,23 +1,14 @@
import { useEffect, useMemo, useState } from "react";
import { App } from "@capacitor/app";
import {
- ExpandMore,
PauseCircleFilled,
PlayCircleFilled,
Refresh,
} from "@mui/icons-material";
import {
- Accordion,
- AccordionDetails,
- AccordionSummary,
Button,
CardActions,
CardMedia,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableRow,
} from "@mui/material";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
@@ -26,16 +17,10 @@ import Typography from "@mui/material/Typography";
import { ShazamTrack } from "shazam-api/dist/types";
import { ImageWithFallback } from "../../Common/ImageWithFallback";
-import { useShazarrProvider } from "../useShazarr";
-export default function CardResult({ data }: { data: ShazamTrack }) {
+export default function CardResult({ data, onClose }: { data: ShazamTrack; onClose: () => void }) {
const [isPlaying, setIsPlaying] = useState();
- const lyrics = useMemo(
- () => data?.sections?.filter((section) => section.type === "LYRICS")?.[0]?.text,
- [data],
- );
-
const sampleURI = useMemo(
() => data?.hub?.actions?.filter((action) => !!action.uri)?.[0]?.uri,
[data],
@@ -45,9 +30,6 @@ export default function CardResult({ data }: { data: ShazamTrack }) {
() => (sampleURI ? new Audio(sampleURI) : undefined),
[sampleURI],
);
- const {
- actions: { resetSearch },
- } = useShazarrProvider();
function togglePlayPause() {
if (!sample) return;
@@ -79,95 +61,54 @@ export default function CardResult({ data }: { data: ShazamTrack }) {
}, [sample]);
return (
- <>
-
-
-
-
-
-
- {data.title}
-
-
- {data.subtitle}
-
-
- {sample && (
-
-
+
+
+
+
+
+ {data.title}
+
+
+ {data.subtitle}
+
+
+ {sample && (
+
+
+
+ ) : (
+
+ )
+ }
+ aria-label={isPlaying ? "pause" : "play"}
+ onClick={togglePlayPause}
>
-
- ) : (
-
- )
- }
- aria-label={isPlaying ? "pause" : "play"}
- onClick={togglePlayPause}
- >
- {isPlaying ? "Pause" : "Play"}
-
-
-
-
-
- )}
-
-
-
-
-
- {data?.sections?.[0]?.metadata?.map((row, index) => (
-
-
- {row.title}
-
- {row.text}
-
- ))}
-
-
-
-
- {lyrics && lyrics?.length > 0 && (
-
-
- }
- aria-controls="panel1a-content"
- id="panel1a-header"
+ {isPlaying ? "Pause" : "Play"}
+
+
+
-
+
+
+
+
)}
- >
+
);
}
diff --git a/src/components/Shazarr/ui/InlineResultCard.tsx b/src/components/Shazarr/ui/InlineResultCard.tsx
new file mode 100644
index 0000000..a59cbda
--- /dev/null
+++ b/src/components/Shazarr/ui/InlineResultCard.tsx
@@ -0,0 +1,46 @@
+import { Close, RemoveRedEye } from "@mui/icons-material";
+import { Box, ButtonBase, Card, CardActions, CardContent, CardMedia, IconButton, Typography } from "@mui/material";
+import { ShazamTrack } from "shazam-api/dist/types";
+
+import { ImageWithFallback } from "../../Common/ImageWithFallback";
+
+interface Props {
+ track: ShazamTrack;
+ onDismiss: () => void;
+ onOpen: () => void;
+}
+
+export default function InlineResultCard({ track, onDismiss, onOpen }: Props) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ {track.title}
+ {track.subtitle && <>
{track.subtitle}>}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Shazarr/ui/StatusChip.tsx b/src/components/Shazarr/ui/StatusChip.tsx
index 71a6c7a..6e51d4b 100644
--- a/src/components/Shazarr/ui/StatusChip.tsx
+++ b/src/components/Shazarr/ui/StatusChip.tsx
@@ -6,27 +6,25 @@ import { useConfigProvider } from "../../Config/useConfig";
import { useShazarrProvider } from "../useShazarr";
export default function StatusChip() {
- const { shazarrLoading, shazarrResponse, recordingStatus } =
+ const { shazarrLoading, showInlineResult, recordingStatus } =
useShazarrProvider();
const { isNetworkConnected } = useConfigProvider();
return (
- ) : shazarrResponse ? (
+ ) : showInlineResult ? (
) : !isNetworkConnected ? (
@@ -37,13 +35,11 @@ export default function StatusChip() {
label={
recordingStatus && recordingStatus !== "inactive"
? `${recordingStatus}...`
- : shazarrResponse
+ : showInlineResult
? "Found !"
- : shazarrResponse && !shazarrResponse
- ? "Not found. Get closer."
- : !isNetworkConnected
- ? "Offline mode"
- : "Ready"
+ : !isNetworkConnected
+ ? "Offline mode"
+ : "Ready"
}
variant="outlined"
/>
diff --git a/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-1-Android-Chrome-linux.png b/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-1-Android-Chrome-linux.png
index ee42fa9..ccf7723 100644
Binary files a/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-1-Android-Chrome-linux.png and b/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-1-Android-Chrome-linux.png differ
diff --git a/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-2-Android-Chrome-linux.png b/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-2-Android-Chrome-linux.png
index e8693fa..dbff4b3 100644
Binary files a/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-2-Android-Chrome-linux.png and b/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-2-Android-Chrome-linux.png differ
diff --git a/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-3-Android-Chrome-linux.png b/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-3-Android-Chrome-linux.png
index 7d19799..42db13b 100644
Binary files a/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-3-Android-Chrome-linux.png and b/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-3-Android-Chrome-linux.png differ
diff --git a/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-4-Android-Chrome-linux.png b/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-4-Android-Chrome-linux.png
new file mode 100644
index 0000000..42db13b
Binary files /dev/null and b/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-4-Android-Chrome-linux.png differ
diff --git a/testing/snapshots/history.spec.ts-snapshots/History-Should-be-able-to-consult-2-different-items-1-Android-Chrome-linux.png b/testing/snapshots/history.spec.ts-snapshots/History-Should-be-able-to-consult-2-different-items-1-Android-Chrome-linux.png
index eb53cf1..fe301fa 100644
Binary files a/testing/snapshots/history.spec.ts-snapshots/History-Should-be-able-to-consult-2-different-items-1-Android-Chrome-linux.png and b/testing/snapshots/history.spec.ts-snapshots/History-Should-be-able-to-consult-2-different-items-1-Android-Chrome-linux.png differ
diff --git a/testing/snapshots/history.spec.ts-snapshots/History-Should-be-able-to-consult-2-different-items-2-Android-Chrome-linux.png b/testing/snapshots/history.spec.ts-snapshots/History-Should-be-able-to-consult-2-different-items-2-Android-Chrome-linux.png
index 0e525c1..bff6b4f 100644
Binary files a/testing/snapshots/history.spec.ts-snapshots/History-Should-be-able-to-consult-2-different-items-2-Android-Chrome-linux.png and b/testing/snapshots/history.spec.ts-snapshots/History-Should-be-able-to-consult-2-different-items-2-Android-Chrome-linux.png differ
diff --git a/testing/snapshots/options.spec.ts-snapshots/Options-Should-see-options-panel-and-use-service-buttons-1-Android-Chrome-linux.png b/testing/snapshots/options.spec.ts-snapshots/Options-Should-see-options-panel-and-use-service-buttons-1-Android-Chrome-linux.png
index 79f4cd1..a326a99 100644
Binary files a/testing/snapshots/options.spec.ts-snapshots/Options-Should-see-options-panel-and-use-service-buttons-1-Android-Chrome-linux.png and b/testing/snapshots/options.spec.ts-snapshots/Options-Should-see-options-panel-and-use-service-buttons-1-Android-Chrome-linux.png differ
diff --git a/testing/snapshots/options.spec.ts-snapshots/Options-Should-see-options-panel-and-use-service-buttons-2-Android-Chrome-linux.png b/testing/snapshots/options.spec.ts-snapshots/Options-Should-see-options-panel-and-use-service-buttons-2-Android-Chrome-linux.png
index 9be4c67..78c55c5 100644
Binary files a/testing/snapshots/options.spec.ts-snapshots/Options-Should-see-options-panel-and-use-service-buttons-2-Android-Chrome-linux.png and b/testing/snapshots/options.spec.ts-snapshots/Options-Should-see-options-panel-and-use-service-buttons-2-Android-Chrome-linux.png differ
diff --git a/testing/tests/critical.spec.ts b/testing/tests/critical.spec.ts
index 8e84ac6..f349270 100644
--- a/testing/tests/critical.spec.ts
+++ b/testing/tests/critical.spec.ts
@@ -9,17 +9,18 @@ test("Critical: Should be able to record, recognize and display result", async (
await expect(page.getByText("Offroad")).toBeVisible();
await expect(page.getByText("Ready")).toBeVisible();
await expect(
- page.getByRole("button", { name: "Services configuration" }),
+ page.getByRole("button", { name: "Configuration" }),
).toBeVisible();
await expect(page).toHaveScreenshot();
// Run song recognition
- await page.getByRole("button").first().click();
+ await page.getByTestId("record-button").click();
await expect(page.getByText("recording...")).toBeVisible();
await page.waitForTimeout(5000);
- // Expect result
+ // Expect inline result card (not a drawer)
await expect(page.getByText("Found !")).toBeVisible();
+ await expect(page.getByTestId("inline-result-card")).toBeVisible();
const loader = await page.locator("img.loading")?.isVisible();
if (loader) {
await expect(page.locator("img.loading")).not.toBeVisible({
@@ -28,6 +29,29 @@ test("Critical: Should be able to record, recognize and display result", async (
}
await page.waitForTimeout(1000);
+ await expect(page.getByTestId("inline-result-card")).toContainText("Chillin'");
+ await expect(page.getByTestId("inline-result-card")).toContainText("Modjo");
+ await expect(page).toHaveScreenshot();
+
+ // Dismiss inline card (close button, NOT delete from history)
+ await page.getByTestId("inline-result-card").getByRole("button", { name: "Dismiss result" }).click();
+ await expect(page.getByTestId("inline-result-card")).not.toBeVisible();
+
+ // Expect to be on main screen with Records button visible
+ await expect(page.getByText("Ready")).toBeVisible();
+ await expect(
+ page.getByRole("button", { name: "Records" }),
+ ).toBeVisible();
+
+ // Open history drawer
+ await page.getByRole("button", { name: "Records" }).click();
+ await expect(page.getByTestId("history-item")).toBeVisible();
+ await expect(page.getByTestId("history-item")).toContainText(
+ "Chillin' - Modjo",
+ );
+
+ // Click history item to open Result drawer
+ await page.getByTestId("history-item").getByRole("button").first().click();
await expect(page.getByText("Chillin'")).toHaveCount(2);
await expect(page.getByText("Modjo", { exact: true })).toBeVisible();
await expect(page).toHaveScreenshot();
@@ -35,28 +59,7 @@ test("Critical: Should be able to record, recognize and display result", async (
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await expect(page).toHaveScreenshot();
- // Expect history
- await page.getByRole("button", { name: "Show records (1)" }).click();
-
- await expect(page.getByTestId("history-item")).toBeVisible();
- await expect(page.getByTestId("history-item")).toContainText(
- "Chillin' - Modjo",
- );
- await expect(page.getByTestId("history-item").locator("button")).toHaveCount(
- 4,
- );
-
- // Expect closing history
- await page.getByRole("button", { name: "Close records" }).click();
- await expect(page.getByTestId("history-item")).not.toBeVisible();
-
- // Expect closing track result
+ // Close Result drawer
await page.getByRole("button", { name: "Close" }).click();
- await expect(page.getByTestId("Close")).not.toBeVisible();
-
- // Expect to be on main screen
await expect(page.getByText("Ready")).toBeVisible();
- await expect(
- page.getByRole("button", { name: "Show records (1)" }),
- ).toBeVisible();
});
diff --git a/testing/tests/history.spec.ts b/testing/tests/history.spec.ts
index c7b60c2..8ba9be5 100644
--- a/testing/tests/history.spec.ts
+++ b/testing/tests/history.spec.ts
@@ -15,7 +15,7 @@ test("History: Should see tracks history", async ({ page }) => {
await expect(page.getByText("Ready")).toBeInViewport();
// Open history panel
- await page.getByRole("button", { name: "Show records (3)" }).click();
+ await page.getByRole("button", { name: "Records" }).click();
await expect(page.getByTestId("history-item")).toHaveCount(3);
const rows = page.getByTestId("history-item");
@@ -25,10 +25,7 @@ test("History: Should see tracks history", async ({ page }) => {
}
// Click on history item
- await page
- .locator("div:nth-child(3) > .MuiCardActions-root > button")
- .first()
- .click();
+ await page.getByTestId("history-item").nth(2).getByRole("button").first().click();
await expect(page.locator(".MuiTypography-h5")).toHaveText("Yakuza");
await expect(page.getByText("Spotify")).toBeVisible();
@@ -47,19 +44,16 @@ test("History: Should be able to remove 1 item", async ({ page }) => {
await expect(page.getByText("Ready")).toBeInViewport();
// Open history panel
- await page.getByRole("button", { name: "Show records (3)" }).click();
+ await page.getByRole("button", { name: "Records" }).click();
// Click on history item
- await page
- .locator("div:nth-child(2) > .MuiCardActions-root > button")
- .last()
- .click();
+ await page.getByTestId("history-item").nth(1).getByRole("button").last().click();
await expect(page.getByTestId("history-item")).toHaveCount(2);
// Refresh and check
await page.reload();
- await page.getByRole("button", { name: "Show records (2)" }).click();
+ await page.getByRole("button", { name: "Records" }).click();
await expect(page.getByText(expectedHistory[0])).toBeVisible();
await expect(page.getByText(expectedHistory[1])).not.toBeVisible();
@@ -73,27 +67,21 @@ test("History: Should be able to remove 2 items", async ({ page }) => {
await expect(page.getByText("Ready")).toBeInViewport();
// Open history panel
- await page.getByRole("button", { name: "Show records (3)" }).click();
+ await page.getByRole("button", { name: "Records" }).click();
// Click on history item
- await page
- .locator("div:nth-child(1) > .MuiCardActions-root > button")
- .last()
- .click();
+ await page.getByTestId("history-item").nth(0).getByRole("button").last().click();
await expect(page.getByTestId("history-item")).toHaveCount(2);
// Click on history item
- await page
- .locator("div:nth-child(1) > .MuiCardActions-root > button")
- .last()
- .click();
+ await page.getByTestId("history-item").nth(0).getByRole("button").last().click();
await expect(page.getByTestId("history-item")).toHaveCount(1);
// Refresh and check
await page.reload();
- await page.getByRole("button", { name: "Show records (1)" }).click();
+ await page.getByRole("button", { name: "Records" }).click();
await expect(page.getByText(expectedHistory[0])).not.toBeVisible();
await expect(page.getByText(expectedHistory[1])).not.toBeVisible();
@@ -109,26 +97,23 @@ test("History: Should be able to consult 2 different items", async ({
await expect(page.getByText("Ready")).toBeInViewport();
// Open history panel
- await page.getByRole("button", { name: "Show records (3)" }).click();
+ await page.getByRole("button", { name: "Records" }).click();
// Click on history item
- await page
- .locator("div:nth-child(1) > .MuiCardActions-root > button")
- .first()
- .click();
+ await page.getByTestId("history-item").nth(0).getByRole("button").first().click();
await expect(
page.getByText("Chillin'ModjoPlay AlbumModjo"),
).toHaveScreenshot({ maxDiffPixelRatio: 0.15 });
+ // Close result before reopening history
+ await page.getByRole("button", { name: "Close" }).click();
+
// Open history panel
- await page.getByRole("button", { name: "Show records (3)" }).click();
+ await page.getByRole("button", { name: "Records" }).click();
// Click on history item
- await page
- .locator("div:nth-child(2) > .MuiCardActions-root > button")
- .first()
- .click();
+ await page.getByTestId("history-item").nth(1).getByRole("button").first().click();
await expect(
page.getByText(
diff --git a/testing/tests/lidarr.spec.ts b/testing/tests/lidarr.spec.ts
index 2e67997..8197cc6 100644
--- a/testing/tests/lidarr.spec.ts
+++ b/testing/tests/lidarr.spec.ts
@@ -24,14 +24,14 @@ async function gotoWithConfig(page: Page, config: Record) {
}
async function openYakuzaResult(page: Page) {
- await page.getByRole("button", { name: "Show records (3)" }).click();
- await page.locator("div:nth-child(3) > .MuiCardActions-root > button").first().click();
+ await page.getByRole("button", { name: "Records" }).click();
+ await page.getByTestId("history-item").nth(2).getByRole("button").first().click();
await expect(page.locator(".MuiTypography-h5")).toHaveText("Yakuza");
}
async function openModjoResult(page: Page) {
- await page.getByRole("button", { name: "Show records (3)" }).click();
- await page.locator("div:nth-child(1) > .MuiCardActions-root > button").first().click();
+ await page.getByRole("button", { name: "Records" }).click();
+ await page.getByTestId("history-item").nth(0).getByRole("button").first().click();
await expect(page.locator(".MuiTypography-h5")).toHaveText("Chillin'");
}
diff --git a/testing/tests/offline.spec.ts b/testing/tests/offline.spec.ts
index 976cb4d..f45c148 100644
--- a/testing/tests/offline.spec.ts
+++ b/testing/tests/offline.spec.ts
@@ -5,20 +5,22 @@ test("Offline: Should save record for further recognition", async ({
}) => {
await page.context().setOffline(false);
await page.goto("/");
+ await page.evaluate(() => window.localStorage.clear());
+ await page.reload();
await expect(page.getByText("Shazarr")).toBeVisible();
+ await expect(page.getByText("Ready", { exact: true })).toBeVisible();
- // Run song recognition and cut internet connection
- await page.getByRole("button").first().click();
- await expect(page.getByText("recording...")).toBeVisible();
await page.context().setOffline(true);
- await page.waitForTimeout(5000);
-
await expect(page.getByText("Offline mode", { exact: true })).toBeVisible();
await expect(page.getByText("Internet is not reachable. In")).toBeVisible();
+ // Run song recognition and cut internet connection
+ await page.getByTestId("record-button").click();
+
+
// Open history panel
- await page.getByRole("button", { name: "Show records (1)" }).click();
+ await page.getByRole("button", { name: "Records" }).click();
await expect(page.getByTestId("history-item")).toHaveCount(1);
await expect(
page
@@ -32,11 +34,12 @@ test("Offline: Should save record for further recognition", async ({
await page.reload();
// Search
- await page.getByRole("button", { name: "Show records (1)" }).click();
+ await page.getByRole("button", { name: "Records" }).click();
await page.getByTestId("history-item").getByRole("button").nth(1).click();
await expect(page.getByText("searching...")).toBeVisible();
await page.waitForTimeout(5000);
await expect(page.getByText("Found !")).toBeVisible();
- await expect(page.getByText("Chillin'")).toHaveCount(2);
- await expect(page.getByText("Modjo", { exact: true })).toBeVisible();
+ await expect(page.getByTestId("inline-result-card")).toBeVisible();
+ await expect(page.getByTestId("inline-result-card")).toContainText("Chillin'");
+ await expect(page.getByTestId("inline-result-card")).toContainText("Modjo");
});
diff --git a/testing/tests/options.spec.ts b/testing/tests/options.spec.ts
index f1ea9d9..e5d6d12 100644
--- a/testing/tests/options.spec.ts
+++ b/testing/tests/options.spec.ts
@@ -52,12 +52,9 @@ test("Options: Should see options panel and use service buttons", async ({
).not.toBeVisible();
// Go to result page
- await page.getByRole("button", { name: "Show records (3)" }).click();
+ await page.getByRole("button", { name: "Records" }).click();
- await page
- .locator("div:nth-child(3) > .MuiCardActions-root > button")
- .first()
- .click();
+ await page.getByTestId("history-item").nth(2).getByRole("button").first().click();
await waitForImgLoaded(page);
await page.waitForTimeout(1000);
@@ -86,10 +83,5 @@ test("Options: Should see options panel and use service buttons", async ({
await page.reload();
await page.getByRole("button", { name: "Configuration" }).click();
- await expect(
- page
- .getByRole("dialog")
- .locator("div")
- .filter({ hasText: "Github page - v0.0.0Save" }),
- ).toHaveScreenshot();
+ await expect(page.getByRole("dialog")).toHaveScreenshot();
});
diff --git a/testing/tests/tidarr.spec.ts b/testing/tests/tidarr.spec.ts
index 4d5e9d4..e70a6ed 100644
--- a/testing/tests/tidarr.spec.ts
+++ b/testing/tests/tidarr.spec.ts
@@ -42,7 +42,7 @@ async function gotoWithConfig(page: Page, config: Record) {
}
async function openYakuzaResult(page: Page) {
- await page.getByRole("button", { name: "Show records (3)" }).click();
+ await page.getByRole("button", { name: "Records" }).click();
await page.locator("div:nth-child(3) > .MuiCardActions-root > button").first().click();
await expect(page.locator(".MuiTypography-h5")).toHaveText("Yakuza");
}