mirror of
https://github.com/thelinkin3000/SonicLair.git
synced 2026-06-11 17:44:46 -04:00
First androidTV views
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
34
src/App.tsx
34
src/App.tsx
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
59
src/Components/NowPlaying.scss
Normal file
59
src/Components/NowPlaying.scss
Normal 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,
|
||||
}
|
||||
}
|
||||
123
src/Components/NowPlaying.tsx
Normal file
123
src/Components/NowPlaying.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
64
src/Components/TVSidebar.scss
Normal file
64
src/Components/TVSidebar.scss
Normal 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;
|
||||
}
|
||||
40
src/Components/TVSidebar.tsx
Normal file
40
src/Components/TVSidebar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user