First androidTV views

This commit is contained in:
Carlos Perez
2022-05-31 13:18:01 -03:00
parent cc9875d057
commit 99560f6eb0
10 changed files with 353 additions and 18 deletions

View File

@@ -7,7 +7,7 @@ android {
applicationId "tech.logica10.soniclair"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 9
versionCode 10
versionName "0.0.1-beta3-vlcbackend-coroutines"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {

View File

@@ -202,4 +202,12 @@ input[type=range]:focus {
input[type=range]:focus::-webkit-slider-runnable-track {
background: #737272;
}
.container-tv{
position:absolute;
top:0;
left: 15vw;
width:85vw;
height:100vh;
}

View File

@@ -24,6 +24,10 @@ import { Capacitor } from '@capacitor/core';
import Search from './Components/Search';
import VLC from './Plugins/VLC';
import Account from './Components/Account';
import NowPlaying from './Components/NowPlaying';
import AndroidTV from './Plugins/AndroidTV';
import classNames from 'classnames';
import TVSidebar from './Components/TVSidebar';
function App() {
@@ -31,6 +35,7 @@ function App() {
const [currentTrack, setCurrentTrack] = useState<IAlbumSongResponse>(CurrentTrackContextDefValue);
const [playlist, setPlaylist] = useState<IAlbumSongResponse[]>([CurrentTrackContextDefValue]);
const [menuContext, setMenuContext] = useState<IMenuContext>(MenuContextDefValue);
const [androidTv, setAndroidTv] = useState<boolean>(false);
const setPlaylistAndPlay = (p: IAlbumSongResponse[], track: number) => {
setPlaylist(p);
setCurrentTrack(p[track]);
@@ -56,6 +61,10 @@ function App() {
text: 'There was an error obtaining a token from spotify. Artist images may look off.',
});
}
if (Capacitor.isPluginAvailable("AndroidTV")) {
setAndroidTv((await AndroidTV.get()).value);
}
const c = await VLC.getActiveAccount();
if (c.status === "ok") {
@@ -68,7 +77,7 @@ function App() {
if (Capacitor.getPlatform() == "android") {
StatusBar.setBackgroundColor({ color: "282c34" });
}
setTried(true);
document.addEventListener("contextmenu", (event) => {
event.preventDefault();
@@ -82,16 +91,20 @@ function App() {
const [navbarCollapsed, setNavbarCollapsed] = useState<boolean>(true);
return (
<div className="App container-fluid d-flex flex-column justify-content-between">
return (<>
{androidTv && <div className="App"><TVSidebar></TVSidebar></div>}
<div className={classNames("App", androidTv ? "container-tv" : "container-fluid", "d-flex", "flex-column", "justify-content-between")}>
<Helmet>
<title>SonicLair</title>
</Helmet>
<MenuContext.Provider value={menuContextValue}>
<CurrentTrackContext.Provider value={currentTrackContextValue}>
<AppContext.Provider value={contextValue}>
<Navbar navbarCollapsed={navbarCollapsed} setNavbarCollapsed={setNavbarCollapsed} />
<Sidebar navbarCollapsed={navbarCollapsed} setNavbarCollapsed={setNavbarCollapsed} />
{!androidTv && <>
<Navbar navbarCollapsed={navbarCollapsed} setNavbarCollapsed={setNavbarCollapsed} />
<Sidebar navbarCollapsed={navbarCollapsed} setNavbarCollapsed={setNavbarCollapsed} /></>}
{
context.username === "" &&
<div className="h-100 w-100 d-flex align-items-center justify-content-center">
@@ -112,15 +125,22 @@ function App() {
<Route path="/account" element={<Account />} />
<Route path="/albums" element={<Albums />} />
<Route path="/search" element={<Search />} />
<Route path="/playing" element={<NowPlaying />} />
</Routes>
<AudioControl />
<CardContextMenu {...menuContext} />
{
!androidTv && <>
<AudioControl />
<CardContextMenu {...menuContext} />
</>
}
</>
}
</AppContext.Provider>
</CurrentTrackContext.Provider>
</MenuContext.Provider>
</div>
</>
);
}

View File

@@ -1,14 +1,16 @@
.current-track-header{
.current-track-header {
height: 10vh;
overflow: hidden;
margin: 0 0 0.5rem 0;
.current-track-img {
max-height: 10vh;
height: auto;
height: auto;
width: auto;
margin: 0 0.5rem 0 0;
}
}
.fade-right{
.fade-right {
// mask-image: linear-gradient(270deg, rgb(0,0,0,0) 0%,rgb(0,0,0,0) 25%,rgb(40, 44, 52) 90%,rgb(40, 44, 52) 100%);
}
}

View File

@@ -6,11 +6,10 @@ import { SecondsToHHSS } from "../Helpers";
import "./AudioControl.scss";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import _, { } from 'lodash';
import { useNavigate } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import classnames from "classnames";
import VLC from "../Plugins/VLC";
import { Backend } from "../Plugins/Audio";
// import AndroidTV from "../Plugins/AndroidTV";
import { Capacitor } from "@capacitor/core";
import AndroidTV from "../Plugins/AndroidTV";
@@ -19,7 +18,6 @@ interface IListener {
func: (ev: any) => void;
}
export default function AudioControl({ }) {
const { currentTrack, setCurrentTrack } = useContext(CurrentTrackContext);
const [isPlaying, setIsPlaying] = useState<boolean>(false);
@@ -27,7 +25,7 @@ export default function AudioControl({ }) {
const [coverArt, setCoverArt] = useState<string>("");
const audioInstance = useRef<Backend>(new Backend());
const [androidTV, setAndroidTV] = useState<boolean>(false);
const location = useLocation();
const navigate = useNavigate();
const [volume, setVolume] = useState<number>(1);
@@ -112,8 +110,15 @@ export default function AudioControl({ }) {
navigate(`/album`, { state: { id: currentTrack.parent } });
}, [currentTrack]);
const hide = useCallback(() => {
if(currentTrack.id === "" || location.pathname.match(/playing/)){
return "d-none"
}
return "d-flex";
},[currentTrack, location.pathname]);
return (
<div className={classnames("flex-column justify-content-between w-100", "mt-3", currentTrack.id === "" ? "d-none" : "d-flex")}>
<div className={classnames("flex-column justify-content-between w-100", "mt-3", hide())}>
<div className="d-flex flex-row align-items-center justify-content-between w-100">
{/* <div className="flex-shrink-1 hide-overflow" > */}
<div onClick={goToAlbum} className={`current-track-header flex-row align-items-center justify-content-start ${currentTrack.id === "" ? "d-none" : "d-flex"}`}>
@@ -126,6 +131,9 @@ export default function AudioControl({ }) {
{/* </div> */}
<div className="d-flex flex-grow-1 flex-column align-items-end justify-content-end">
<div className="d-flex flex-row align-items-center justify-content-center p-0">
<button type="button" className="btn btn-link text-white" onClick={() => navigate("playing")}>
<FontAwesomeIcon flip="horizontal" icon={faForwardStep}></FontAwesomeIcon>
</button>
<button type="button" className="btn btn-link text-white" onClick={playPrev}>
<FontAwesomeIcon flip="horizontal" icon={faForwardStep}></FontAwesomeIcon>
</button>

View File

@@ -0,0 +1,59 @@
.current-track-img-tv {
max-height: 40vh;
height: auto;
width: auto;
margin: 0 0.5rem 0 0;
border-radius: 5%;
box-shadow:
0 0 10px 5px #fff,
0 0 15px 10px #0ff;
animation: pulse 2s infinite;
}
.current-track-header {
overflow: hidden;
margin: 0 0 0.5rem 0;
}
.fade-right {
// mask-image: linear-gradient(270deg, rgb(0,0,0,0) 0%,rgb(0,0,0,0) 25%,rgb(40, 44, 52) 90%,rgb(40, 44, 52) 100%);
}
.shadow-down {
width: 100vw;
height: 1px;
position: absolute;
left: 0;
top: calc(100vh - 1px);
box-shadow:
0 0 15px 10px #fff,
0 0 20px 15px #0ff,
}
@keyframes pulse {
0% {
box-shadow:
0 0 15px 10px #fff,
0 0 20px 15px #0ff,
}
25% {
box-shadow:
0 0 15px 10px #0ff,
0 0 20px 15px #7df,
}
50% {
box-shadow:
0 0 15px 10px #7df,
0 0 20px 15px #478499,
}
75% {
box-shadow:
0 0 15px 10px #478499,
0 0 20px 15px #fff,
}
}

View File

@@ -0,0 +1,123 @@
import { faForwardStep, faPause, faPlay, faRotateLeft, faRotateRight } from "@fortawesome/free-solid-svg-icons";
import { ChangeEvent, useCallback, useContext, useEffect, useState } from "react";
import { CurrentTrackContext } from "../AudioContext"
import { SecondsToHHSS } from "../Helpers";
import "./NowPlaying.scss";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import _, { } from 'lodash';
import { useNavigate } from "react-router-dom";
import VLC from "../Plugins/VLC";
// import AndroidTV from "../Plugins/AndroidTV";
export default function NowPlaying({ }) {
const { currentTrack, setCurrentTrack } = useContext(CurrentTrackContext);
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [playTime, setPlayTime] = useState<number>(0);
const [coverArt, setCoverArt] = useState<string>("");
const [androidTV, setAndroidTV] = useState<boolean>(false);
const changePlayTime = useCallback((e: ChangeEvent<HTMLInputElement>): void => {
const time = parseFloat(e.target.value);
VLC.seek({ time: time });
}, [currentTrack]);
useEffect(() => {
if (currentTrack.id === "") {
return;
}
const fetch = async () => {
setCoverArt((await VLC.getAlbumArt({ id: currentTrack.coverArt })).value!);
}
fetch();
}, [currentTrack, coverArt]);
const playNext = useCallback(() => {
VLC.next();
}, []);
const playPrev = useCallback(() => {
VLC.prev();
}, []);
const togglePlaying = () => {
if (isPlaying) {
VLC.pause();
}
else {
VLC.play();
}
};
useEffect(() => {
(VLC as any).removeAllListeners();
// I'm sorry typescript gods.
(VLC as any).addListener('play', (info: any) => {
setIsPlaying(true);
});
(VLC as any).addListener('paused', (info: any) => {
setIsPlaying(false);
});
(VLC as any).addListener('stopped', (info: any) => {
setIsPlaying(false);
});
(VLC as any).addListener('progress', (info: any) => {
setPlayTime(info.time);
});
(VLC as any).addListener('currentTrack', (info: any) => {
setCurrentTrack(info.currentTrack);
});
return () => {
//setCurrentTrack(CurrentTrackContextDefValue);
}
}, [currentTrack, isPlaying, setIsPlaying, setCurrentTrack]);
return (
<div className={"d-flex flex-column align-items-center justify-content-between h-100"}>
<div className="m-auto"></div>
<img className={"current-track-img-tv"} src={coverArt}></img>
<div className={`current-track-header flex-row align-items-center justify-content-start`}>
<div className="ml-2 flex-shrink-5 h-100 d-flex flex-column align-items-start justify-content-end text-center fade-right" >
<span className="text-white no-wrap w-100 section-header" style={{ overflow: "hidden", whiteSpace: "nowrap", fontWeight: 800 }}>{currentTrack.title}</span>
<span className="text-white no-wrap mb-0 w-100" style={{ overflow: "hidden", whiteSpace: "nowrap" }}>{currentTrack.album} - by {currentTrack.artist}</span>
</div>
</div>
<div className="m-auto"></div>
<div className="d-flex flex-row align-items-start justify-content-center p-0">
<button type="button" className="btn btn-link text-white" onClick={playNext}>
<div className="d-flex flex-column align-items-center ">
<FontAwesomeIcon size="2x" icon={faRotateLeft}></FontAwesomeIcon>10s
</div>
</button>
<button type="button" className="btn btn-link text-white" onClick={playPrev}>
<FontAwesomeIcon flip="horizontal" size="2x" icon={faForwardStep}></FontAwesomeIcon>
</button>
<button type="button" className="btn btn-link text-white" onClick={togglePlaying}>
{isPlaying ?
<FontAwesomeIcon size="2x" icon={faPause}></FontAwesomeIcon> :
<FontAwesomeIcon size="2x" icon={faPlay}></FontAwesomeIcon>
}
</button>
<button type="button" className="btn btn-link text-white" onClick={playNext}>
<FontAwesomeIcon size="2x" icon={faForwardStep}></FontAwesomeIcon>
</button>
<button type="button" className="btn btn-link text-white" onClick={playNext}>
<div className="d-flex flex-column align-items-center">
<FontAwesomeIcon size="2x" icon={faRotateRight}></FontAwesomeIcon>
<span style={{textDecorationLine:"none important!"}}>10s</span>
</div>
</button>
</div>
<div className="w-50 d-flex flex-row justify-content-between text-white">
<span>{SecondsToHHSS((playTime ?? 0) * (currentTrack?.duration ?? 0))}</span>
<span>{SecondsToHHSS(currentTrack.duration)}</span>
</div>
<div className="w-50" style={{ marginBottom: "30px" }}>
<input disabled type="range" className="w-100" min={0} max={1} step={0.01} value={playTime} onChange={(e) => changePlayTime(e)}></input>
</div>
</div>
)
}

View File

@@ -0,0 +1,64 @@
@import "../Styles/colors.scss";
.sidebar-tv {
width: 15vw;
height: 100vh;
position: absolute;
left: 0;
top: 0;
transition: ease 0.3s;
margin: 0 0 0 0;
.sidebar-item {
width: 15vw;
height: 4rem;
background-color: $items-color;
margin-bottom: 0.5rem;
transition: ease 0.3s;
background: linear-gradient(to right, rgba(0, 0, 0, 0),rgba(40, 44, 52, 1));
&:hover {
background-color: $items-color-hover;
}
.sidebar-item.focused{
background-color: $items-color-hover;
}
svg {
font-size: 2.5rem;
}
.item-text{
margin-left: 10px;
font-size: large;
}
}
.sidebar-item-borderless {
width: 4rem;
height: 4rem;
border-radius: 10%;
margin-bottom: 0.5rem;
transition: ease 0.3s;
&:hover {
background-color: $items-color-hover;
}
svg {
font-size: 2.5rem;
}
}
}
.modal-cover{
position:fixed;
left:0;
top:0;
background-color: $item-background-translucid;
width:100vw;
height:100vh;
z-index: 2000;
transition: ease 0.1s;
}

View File

@@ -0,0 +1,40 @@
import { faHouseChimney, faMagnifyingGlass, faUserAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import "./TVSidebar.scss";
import { useNavigate } from "react-router-dom";
import { faPlayCircle } from "@fortawesome/free-regular-svg-icons";
export default function TVSidebar() {
const navigate = useNavigate();
const nav = (path: string) => {
navigate(path);
};
return (
<div className="sidebar-tv d-flex flex-column">
<div onClick={() => nav("/home")}
className="sidebar-item d-flex align-items-center justify-content-start text-white p-3 mt-3">
<FontAwesomeIcon icon={faHouseChimney} />
<span className="item-text">Home</span>
</div>
<div onClick={() => nav("/search")}
className="sidebar-item d-flex align-items-center justify-content-start text-white p-3">
<FontAwesomeIcon icon={faMagnifyingGlass} />
<span className="item-text">Search</span>
</div>
<div onClick={() => nav("/account")}
className="sidebar-item last-item d-flex align-items-center justify-content-start text-white p-3">
<FontAwesomeIcon icon={faUserAlt} />
<span className="item-text">Account</span>
</div>
<div className="m-auto"></div>
<div onClick={() => nav("/playing")}
className="sidebar-item last-item d-flex align-items-center justify-content-start text-white p-3 mb-3">
<FontAwesomeIcon icon={faPlayCircle} />
<span className="item-text">Playing</span>
</div>
</div>
)
}

View File

@@ -8,6 +8,17 @@ export interface IBackendPlugin {
get(): Promise<IAndroidTVResponse>;
}
const AndroidTV = registerPlugin<IBackendPlugin>('AndroidTV');
class AndroidTVPlugin implements IBackendPlugin {
constructor() {
}
get(): Promise<IAndroidTVResponse> {
return Promise.resolve({ value: true });
}
}
const AndroidTV = registerPlugin<IBackendPlugin>('AndroidTV', {
web: () => new AndroidTVPlugin(),
});
export default AndroidTV;