Udded player progress, updated UI.
Signed-off-by: Pavel Kirilin <win10@list.ru>
This commit is contained in:
@ -1,13 +1,15 @@
|
|||||||
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
import dbus
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from typer import Argument, Typer
|
from typer import Argument, Typer
|
||||||
|
from anime.router import router
|
||||||
from anime.routes import router
|
from anime import lifespan
|
||||||
|
|
||||||
CURRENT_DIR = Path(__file__).parent
|
CURRENT_DIR = Path(__file__).parent
|
||||||
STATIC_DIR = CURRENT_DIR / "static"
|
STATIC_DIR = CURRENT_DIR / "static"
|
||||||
@ -35,6 +37,10 @@ def run_app(
|
|||||||
description="API for RCE",
|
description="API for RCE",
|
||||||
openapi_url="/api/openapi.json",
|
openapi_url="/api/openapi.json",
|
||||||
docs_url="/api/docs",
|
docs_url="/api/docs",
|
||||||
|
lifespan=partial(
|
||||||
|
lifespan.lifespan,
|
||||||
|
anime_dir=anime_dir,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
app.include_router(router, prefix="/api")
|
app.include_router(router, prefix="/api")
|
||||||
app.mount(
|
app.mount(
|
||||||
@ -46,9 +52,6 @@ def run_app(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
app.add_exception_handler(Exception, default_exception_handler)
|
app.add_exception_handler(Exception, default_exception_handler)
|
||||||
app.state.anime_dir = anime_dir
|
|
||||||
app.state.pid = None
|
|
||||||
|
|
||||||
uvicorn.run(app, host=host, port=port, workers=1)
|
uvicorn.run(app, host=host, port=port, workers=1)
|
||||||
|
|
||||||
|
|
||||||
|
6
anime/deps.py
Normal file
6
anime/deps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
from fastapi import Depends
|
||||||
|
from anime import services
|
||||||
|
|
||||||
|
|
||||||
|
GetDbusService = Annotated[services.DbusService, Depends()]
|
@ -1,10 +1,25 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class PlayerOffsetRequest(BaseModel):
|
class PlayerRequest(BaseModel):
|
||||||
|
player: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerOffsetRequest(PlayerRequest):
|
||||||
offset: int
|
offset: int
|
||||||
forward: bool
|
forward: bool
|
||||||
|
|
||||||
|
|
||||||
class KillRequest(BaseModel):
|
class KillRequest(BaseModel):
|
||||||
names: list[str]
|
names: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerInfo(BaseModel):
|
||||||
|
name: str
|
||||||
|
playing: bool
|
||||||
|
can_seek: bool
|
||||||
|
artists: list[str]
|
||||||
|
|
||||||
|
progress: int | None = None
|
||||||
|
title: str | None = None
|
||||||
|
album: str | None = None
|
||||||
|
15
anime/lifespan.py
Normal file
15
anime/lifespan.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
import dbus
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI, anime_dir: Path):
|
||||||
|
app.state.anime_dir = anime_dir
|
||||||
|
app.state.pid = None
|
||||||
|
app.state.dbus_session = dbus.SessionBus()
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
app.state.dbus_session.close()
|
7
anime/router/__init__.py
Normal file
7
anime/router/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from . import player, commands
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
router.include_router(player.router, prefix="/player")
|
||||||
|
router.include_router(commands.router, prefix="/commands")
|
46
anime/router/commands.py
Normal file
46
anime/router/commands.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException, Request
|
||||||
|
|
||||||
|
from anime import dtos
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def is_pid_alive(pid: int) -> bool:
|
||||||
|
if pid:
|
||||||
|
try:
|
||||||
|
os.kill(pid, 0)
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/can-start-watching")
|
||||||
|
def can_start_watching(request: Request) -> None:
|
||||||
|
if not request.app.state.anime_dir:
|
||||||
|
raise HTTPException(status_code=400, detail="Anime directory is not set")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/kill")
|
||||||
|
def kill(input_dto: dtos.KillRequest) -> None:
|
||||||
|
for name in input_dto.names:
|
||||||
|
os.system(f"killall {name}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/start-watching")
|
||||||
|
def start_watching(request: Request) -> None:
|
||||||
|
anime_dir = request.app.state.anime_dir
|
||||||
|
if not anime_dir:
|
||||||
|
raise HTTPException(status_code=400, detail="Anime directory is not set")
|
||||||
|
awatch = shutil.which("awatch")
|
||||||
|
if awatch is None:
|
||||||
|
raise Exception("awatch command is not available")
|
||||||
|
ret = subprocess.Popen(
|
||||||
|
[awatch],
|
||||||
|
cwd=anime_dir,
|
||||||
|
)
|
||||||
|
request.app.state.pid = ret.pid
|
28
anime/router/player.py
Normal file
28
anime/router/player.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from anime import dtos
|
||||||
|
from anime.deps import GetDbusService
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/offset")
|
||||||
|
async def offset(
|
||||||
|
req: dtos.PlayerOffsetRequest,
|
||||||
|
dbus_service: GetDbusService,
|
||||||
|
) -> None:
|
||||||
|
direction = 1 if req.forward else -1
|
||||||
|
dbus_service.seek(abs(req.offset) * direction, req.player)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/play-pause")
|
||||||
|
async def play_pause(
|
||||||
|
input_dto: dtos.PlayerRequest,
|
||||||
|
dbus_service: GetDbusService,
|
||||||
|
) -> None:
|
||||||
|
dbus_service.play_pause(input_dto.player)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/list")
|
||||||
|
def get_players(dbus_service: GetDbusService) -> list[dtos.PlayerInfo]:
|
||||||
|
return dbus_service.list_players()
|
102
anime/routes.py
102
anime/routes.py
@ -1,102 +0,0 @@
|
|||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Request
|
|
||||||
|
|
||||||
from anime.dtos import KillRequest, PlayerOffsetRequest
|
|
||||||
|
|
||||||
CWD = Path.cwd()
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
def is_pid_alive(pid: int) -> bool:
|
|
||||||
if pid:
|
|
||||||
try:
|
|
||||||
os.kill(pid, 0)
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/can-start-watching")
|
|
||||||
def can_start_watching(request: Request) -> None:
|
|
||||||
if not request.app.state.anime_dir:
|
|
||||||
raise HTTPException(status_code=400, detail="Anime directory is not set")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/kill")
|
|
||||||
def kill(input_dto: KillRequest) -> None:
|
|
||||||
for name in input_dto.names:
|
|
||||||
os.system(f"killall {name}")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/start-watching")
|
|
||||||
def start_watching(request: Request) -> None:
|
|
||||||
anime_dir = request.app.state.anime_dir
|
|
||||||
if not anime_dir:
|
|
||||||
raise HTTPException(status_code=400, detail="Anime directory is not set")
|
|
||||||
if request.app.state.pid and is_pid_alive(request.app.state.pid):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="Awatch is already running",
|
|
||||||
)
|
|
||||||
awatch = shutil.which("awatch")
|
|
||||||
if awatch is None:
|
|
||||||
raise Exception("awatch command is not available")
|
|
||||||
ret = subprocess.Popen(
|
|
||||||
[awatch],
|
|
||||||
cwd=anime_dir,
|
|
||||||
)
|
|
||||||
request.app.state.pid = ret.pid
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/player/offset")
|
|
||||||
async def offset(req: PlayerOffsetRequest) -> None:
|
|
||||||
direction = "+" if req.forward else "-"
|
|
||||||
playerctl = shutil.which("playerctl")
|
|
||||||
if playerctl is None:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=500,
|
|
||||||
detail="playerctl command is not available",
|
|
||||||
)
|
|
||||||
|
|
||||||
subprocess.run(
|
|
||||||
[playerctl, "position", f"{req.offset}{direction}"],
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/player/play-pause")
|
|
||||||
async def play_pause() -> None:
|
|
||||||
playerctl = shutil.which("playerctl")
|
|
||||||
if playerctl is None:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=500,
|
|
||||||
detail="playerctl command is not available",
|
|
||||||
)
|
|
||||||
|
|
||||||
subprocess.run([playerctl, "play-pause"], check=False)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/player/playing")
|
|
||||||
def player_state(request: Request):
|
|
||||||
playerctl = shutil.which("playerctl")
|
|
||||||
if playerctl is None:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=500,
|
|
||||||
detail="playerctl command is not available",
|
|
||||||
)
|
|
||||||
|
|
||||||
status_cmd = subprocess.Popen([playerctl, "status"], stdout=subprocess.PIPE)
|
|
||||||
status_cmd.wait()
|
|
||||||
current_state = status_cmd.stdout.read().decode("utf-8").strip().lower()
|
|
||||||
if (
|
|
||||||
request.app.state.pid
|
|
||||||
and is_pid_alive(request.app.state.pid)
|
|
||||||
and current_state == "playing"
|
|
||||||
):
|
|
||||||
return {"playing": True}
|
|
||||||
return {"playing": False}
|
|
5
anime/services/__init__.py
Normal file
5
anime/services/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from .dbus import DbusService
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"DbusService",
|
||||||
|
]
|
131
anime/services/dbus.py
Normal file
131
anime/services/dbus.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
import dbus.service
|
||||||
|
from fastapi import Depends, HTTPException, Request
|
||||||
|
import dbus
|
||||||
|
|
||||||
|
from anime import dtos
|
||||||
|
|
||||||
|
|
||||||
|
def _get_dbus_session(request: Request) -> dbus.SessionBus:
|
||||||
|
return request.app.state.dbus_session
|
||||||
|
|
||||||
|
|
||||||
|
class DbusService:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
dbus_session: Annotated[dbus.SessionBus, Depends(_get_dbus_session)],
|
||||||
|
) -> None:
|
||||||
|
self.session = dbus_session
|
||||||
|
|
||||||
|
def _get_player(self, name: str) -> dbus.service.Object:
|
||||||
|
if name:
|
||||||
|
try:
|
||||||
|
return self.session.get_object(
|
||||||
|
f"org.mpris.MediaPlayer2.{name}",
|
||||||
|
"/org/mpris/MediaPlayer2",
|
||||||
|
)
|
||||||
|
except dbus.exceptions.DBusException as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Player {name} is not available",
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
for name in self.session.list_names():
|
||||||
|
if not name.startswith("org.mpris.MediaPlayer2."):
|
||||||
|
continue
|
||||||
|
return self.session.get_object(name, "/org/mpris/MediaPlayer2")
|
||||||
|
raise HTTPException(status_code=400, detail="No player is available")
|
||||||
|
|
||||||
|
def _resolve_name(self, name: str | None = None) -> str:
|
||||||
|
if name is None:
|
||||||
|
all_players = [
|
||||||
|
name
|
||||||
|
for name in self.session.list_names()
|
||||||
|
if name.startswith("org.mpris.MediaPlayer2.")
|
||||||
|
]
|
||||||
|
if not all_players:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="No player is available",
|
||||||
|
)
|
||||||
|
return all_players[0].removeprefix("org.mpris.MediaPlayer2.")
|
||||||
|
if name:
|
||||||
|
return name
|
||||||
|
|
||||||
|
def list_players(self) -> list[dtos.PlayerInfo]:
|
||||||
|
state = []
|
||||||
|
for name in self.session.list_names():
|
||||||
|
if not name.startswith("org.mpris.MediaPlayer2."):
|
||||||
|
continue
|
||||||
|
player = self.session.get_object(name, "/org/mpris/MediaPlayer2")
|
||||||
|
status = player.Get(
|
||||||
|
"org.mpris.MediaPlayer2.Player",
|
||||||
|
"PlaybackStatus",
|
||||||
|
dbus_interface="org.freedesktop.DBus.Properties",
|
||||||
|
)
|
||||||
|
metadata = player.Get(
|
||||||
|
"org.mpris.MediaPlayer2.Player",
|
||||||
|
"Metadata",
|
||||||
|
dbus_interface="org.freedesktop.DBus.Properties",
|
||||||
|
)
|
||||||
|
can_seek = player.Get(
|
||||||
|
"org.mpris.MediaPlayer2.Player",
|
||||||
|
"CanSeek",
|
||||||
|
dbus_interface="org.freedesktop.DBus.Properties",
|
||||||
|
)
|
||||||
|
track_len = metadata.get("mpris:length")
|
||||||
|
progress = None
|
||||||
|
try:
|
||||||
|
position = player.Get(
|
||||||
|
"org.mpris.MediaPlayer2.Player",
|
||||||
|
"Position",
|
||||||
|
dbus_interface="org.freedesktop.DBus.Properties",
|
||||||
|
)
|
||||||
|
if position is not None and track_len is not None:
|
||||||
|
progress = ((position * 100) // track_len)
|
||||||
|
except dbus.exceptions.DBusException:
|
||||||
|
pass
|
||||||
|
artists = metadata.get("xesam:artist")
|
||||||
|
album = metadata.get("xesam:album")
|
||||||
|
title = metadata.get("xesam:title")
|
||||||
|
state.append(
|
||||||
|
dtos.PlayerInfo(
|
||||||
|
name=name.removeprefix("org.mpris.MediaPlayer2."),
|
||||||
|
playing=status.lower() == "playing",
|
||||||
|
can_seek=bool(can_seek),
|
||||||
|
artists=[str(artist) for artist in artists] if artists else [],
|
||||||
|
title=str(title) if title else None,
|
||||||
|
album=str(album) if album else None,
|
||||||
|
progress=progress,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return state
|
||||||
|
|
||||||
|
def seek(
|
||||||
|
self,
|
||||||
|
offset: int,
|
||||||
|
name: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
resolved_name = self._resolve_name(name)
|
||||||
|
player = self._get_player(resolved_name)
|
||||||
|
try:
|
||||||
|
player.Seek(
|
||||||
|
offset * 1_000_000,
|
||||||
|
dbus_interface="org.mpris.MediaPlayer2.Player",
|
||||||
|
)
|
||||||
|
except dbus.exceptions.DBusException as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Player {resolved_name} is not seekable",
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
def play_pause(self, name: str | None = None) -> None:
|
||||||
|
resolved_name = self._resolve_name(name)
|
||||||
|
player = self._get_player(resolved_name)
|
||||||
|
try:
|
||||||
|
player.PlayPause(dbus_interface="org.mpris.MediaPlayer2.Player")
|
||||||
|
except dbus.exceptions.DBusException as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Player {resolved_name} cannot be controlled",
|
||||||
|
) from exc
|
@ -6,8 +6,13 @@
|
|||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Anime</title>
|
<title>Anime</title>
|
||||||
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
|
<link rel="mask-icon" href="/mask-icon.svg" color="#F0EFF4">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#F0EFF4">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: "#F0EFF4";
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@qvant/qui-max": "^0.19.0",
|
||||||
|
"@vueuse/core": "^10.9.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vant": "^4.8.11",
|
"vant": "^4.8.11",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
|
84
frontend/pnpm-lock.yaml
generated
84
frontend/pnpm-lock.yaml
generated
@ -5,6 +5,12 @@ settings:
|
|||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@qvant/qui-max':
|
||||||
|
specifier: ^0.19.0
|
||||||
|
version: 0.19.0(vue@3.4.26)
|
||||||
|
'@vueuse/core':
|
||||||
|
specifier: ^10.9.0
|
||||||
|
version: 10.9.0(vue@3.4.26)
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^2.1.7
|
specifier: ^2.1.7
|
||||||
version: 2.1.7(vue@3.4.26)
|
version: 2.1.7(vue@3.4.26)
|
||||||
@ -1271,7 +1277,6 @@ packages:
|
|||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime: 0.14.1
|
regenerator-runtime: 0.14.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@babel/template@7.24.0:
|
/@babel/template@7.24.0:
|
||||||
resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==}
|
resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==}
|
||||||
@ -1638,6 +1643,25 @@ packages:
|
|||||||
resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==}
|
resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@popperjs/core@2.11.8:
|
||||||
|
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@qvant/qui-max@0.19.0(vue@3.4.26):
|
||||||
|
resolution: {integrity: sha512-ZSql2VXq2GLDSldo0ezicT6NpIM+VCjy8yK4sg/dDBUeEqxGyWkM5O6zdGI2v2Q2UDJkAx83wLewTXcWYIDR6w==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.2.33
|
||||||
|
dependencies:
|
||||||
|
'@popperjs/core': 2.11.8
|
||||||
|
async-validator: 4.2.5
|
||||||
|
colord: 2.9.3
|
||||||
|
date-fns: 2.30.0
|
||||||
|
focus-visible: 5.2.0
|
||||||
|
lodash-es: 4.17.21
|
||||||
|
object-hash: 3.0.0
|
||||||
|
vue: 3.4.26
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@rollup/plugin-babel@5.3.1(@babel/core@7.24.5)(rollup@2.79.1):
|
/@rollup/plugin-babel@5.3.1(@babel/core@7.24.5)(rollup@2.79.1):
|
||||||
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
|
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
@ -1882,6 +1906,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/web-bluetooth@0.0.20:
|
||||||
|
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@ungap/structured-clone@1.2.0:
|
/@ungap/structured-clone@1.2.0:
|
||||||
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -2065,6 +2093,31 @@ packages:
|
|||||||
/@vue/shared@3.4.26:
|
/@vue/shared@3.4.26:
|
||||||
resolution: {integrity: sha512-Fg4zwR0GNnjzodMt3KRy2AWGMKQXByl56+4HjN87soxLNU9P5xcJkstAlIeEF3cU6UYOzmJl1tV0dVPGIljCnQ==}
|
resolution: {integrity: sha512-Fg4zwR0GNnjzodMt3KRy2AWGMKQXByl56+4HjN87soxLNU9P5xcJkstAlIeEF3cU6UYOzmJl1tV0dVPGIljCnQ==}
|
||||||
|
|
||||||
|
/@vueuse/core@10.9.0(vue@3.4.26):
|
||||||
|
resolution: {integrity: sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/web-bluetooth': 0.0.20
|
||||||
|
'@vueuse/metadata': 10.9.0
|
||||||
|
'@vueuse/shared': 10.9.0(vue@3.4.26)
|
||||||
|
vue-demi: 0.14.7(vue@3.4.26)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@vue/composition-api'
|
||||||
|
- vue
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@vueuse/metadata@10.9.0:
|
||||||
|
resolution: {integrity: sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@vueuse/shared@10.9.0(vue@3.4.26):
|
||||||
|
resolution: {integrity: sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==}
|
||||||
|
dependencies:
|
||||||
|
vue-demi: 0.14.7(vue@3.4.26)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@vue/composition-api'
|
||||||
|
- vue
|
||||||
|
dev: false
|
||||||
|
|
||||||
/acorn-jsx@5.3.2(acorn@8.11.3):
|
/acorn-jsx@5.3.2(acorn@8.11.3):
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -2142,6 +2195,10 @@ packages:
|
|||||||
is-shared-array-buffer: 1.0.3
|
is-shared-array-buffer: 1.0.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/async-validator@4.2.5:
|
||||||
|
resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/async@3.2.5:
|
/async@3.2.5:
|
||||||
resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
|
resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -2312,6 +2369,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/colord@2.9.3:
|
||||||
|
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/commander@2.20.3:
|
/commander@2.20.3:
|
||||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -2385,6 +2446,13 @@ packages:
|
|||||||
is-data-view: 1.0.1
|
is-data-view: 1.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/date-fns@2.30.0:
|
||||||
|
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
|
||||||
|
engines: {node: '>=0.11'}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.24.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
/debug@4.3.4:
|
/debug@4.3.4:
|
||||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@ -2842,6 +2910,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
|
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/focus-visible@5.2.0:
|
||||||
|
resolution: {integrity: sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/for-each@0.3.3:
|
/for-each@0.3.3:
|
||||||
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -3375,6 +3447,10 @@ packages:
|
|||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash-es@4.17.21:
|
||||||
|
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.debounce@4.0.8:
|
/lodash.debounce@4.0.8:
|
||||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -3489,6 +3565,11 @@ packages:
|
|||||||
boolbase: 1.0.0
|
boolbase: 1.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/object-hash@3.0.0:
|
||||||
|
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/object-inspect@1.13.1:
|
/object-inspect@1.13.1:
|
||||||
resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
|
resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -3698,7 +3779,6 @@ packages:
|
|||||||
|
|
||||||
/regenerator-runtime@0.14.1:
|
/regenerator-runtime@0.14.1:
|
||||||
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/regenerator-transform@0.15.2:
|
/regenerator-transform@0.15.2:
|
||||||
resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==}
|
resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/* color palette from <https://github.com/vuejs/theme> */
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
:root {
|
:root {
|
||||||
--vt-c-white: #ffffff;
|
--vt-c-white: #F0EFF4;
|
||||||
--vt-c-white-soft: #f8f8f8;
|
--vt-c-white-soft: #f8f8f8;
|
||||||
--vt-c-white-mute: #f2f2f2;
|
--vt-c-white-mute: #f2f2f2;
|
||||||
|
|
||||||
@ -36,19 +36,6 @@
|
|||||||
--section-gap: 160px;
|
--section-gap: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--color-background: var(--vt-c-black);
|
|
||||||
--color-background-soft: var(--vt-c-black-soft);
|
|
||||||
--color-background-mute: var(--vt-c-black-mute);
|
|
||||||
|
|
||||||
--color-border: var(--vt-c-divider-dark-2);
|
|
||||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
|
||||||
|
|
||||||
--color-heading: var(--vt-c-text-dark-1);
|
|
||||||
--color-text: var(--vt-c-text-dark-2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
|
@ -1,30 +1,127 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ActionBar, ActionBarButton } from 'vant';
|
import { ActionBar, ActionBarButton, ActionSheet, Progress } from 'vant';
|
||||||
import { postRequest } from '@/utils'
|
import { postRequest } from '@/utils'
|
||||||
import { useBackendStateStore } from '@/stores/backendState';
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { usePlayerStateStore } from '@/stores/playersState';
|
||||||
|
import { onLongPress } from '@vueuse/core'
|
||||||
|
import { onUnmounted } from 'vue';
|
||||||
|
|
||||||
const backendStore = useBackendStateStore();
|
|
||||||
|
const playerStateStore = usePlayerStateStore()
|
||||||
|
const playButton = ref()
|
||||||
|
const showPlayerSelect = ref(false)
|
||||||
|
let updater = null
|
||||||
|
|
||||||
async function offsetRequest(offset, forward) {
|
async function offsetRequest(offset, forward) {
|
||||||
await postRequest(`/api/player/offset`, {
|
await postRequest(`/api/player/offset`, {
|
||||||
offset,
|
offset,
|
||||||
forward,
|
forward,
|
||||||
|
player: playerStateStore.playerState.current_player
|
||||||
})
|
})
|
||||||
await backendStore.updatePlaying()
|
playerStateStore.refreshPlayers()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function playPauseRequest() {
|
async function playPauseRequest() {
|
||||||
await postRequest(`/api/player/play-pause`)
|
await postRequest(`/api/player/play-pause`, {
|
||||||
await backendStore.updatePlaying()
|
player: playerStateStore.playerState.current_player
|
||||||
|
})
|
||||||
|
playerStateStore.refreshPlayers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onPlayerSelect(player) {
|
||||||
|
playerStateStore.setCurrentPlayer(player.name)
|
||||||
|
showPlayerSelect.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProgress() {
|
||||||
|
let player = playerStateStore.getCurrentPlayer()
|
||||||
|
if (player == null) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (player.progress == null) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return player.progress
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasProgress() {
|
||||||
|
let player = playerStateStore.getCurrentPlayer();
|
||||||
|
if (player == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return player.progress !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlayersForChooser() {
|
||||||
|
let players = [];
|
||||||
|
|
||||||
|
for (let player of playerStateStore.playerState.players) {
|
||||||
|
let color = "#000000"
|
||||||
|
if (player.name === playerStateStore.playerState.current_player) {
|
||||||
|
color = "#b83e65"
|
||||||
|
}
|
||||||
|
let subname = [player.artists.join(", "), player.title].join(" - ")
|
||||||
|
let icon = player.playing ? "play" : "pause"
|
||||||
|
players.push({ name: player.name, color, subname, icon })
|
||||||
|
}
|
||||||
|
|
||||||
|
return players
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onLongPress(playButton, (event) => {
|
||||||
|
showPlayerSelect.value = true
|
||||||
|
}, {
|
||||||
|
modifiers: { prevent: true, capture: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
playerStateStore.refreshPlayers()
|
||||||
|
updater = setInterval(() => {
|
||||||
|
playerStateStore.refreshPlayers()
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (updater != null) {
|
||||||
|
clearInterval(updater)
|
||||||
|
updater = null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ActionBar>
|
<div v-if="playerStateStore.playerState.players.length > 0">
|
||||||
<ActionBarButton icon="arrow-double-left" @click="offsetRequest(85, false)"></ActionBarButton>
|
<Progress id="playerProgress" v-if="hasProgress()" :percentage="getProgress()" :show-pivot="false" />
|
||||||
<ActionBarButton icon="arrow-left" @click="offsetRequest(10, false)"></ActionBarButton>
|
<ActionBar id="playerControls">
|
||||||
<ActionBarButton :icon="backendStore.backendState.playing ? 'pause' : 'play'" @click="playPauseRequest()"></ActionBarButton>
|
<ActionSheet v-model:show="showPlayerSelect" :actions="getPlayersForChooser()" cancel-text="Cancel"
|
||||||
<ActionBarButton icon="arrow" @click="offsetRequest(10, true)"></ActionBarButton>
|
description="Available players" @select="onPlayerSelect">
|
||||||
<ActionBarButton icon="arrow-double-right" @click="offsetRequest(85, true)"></ActionBarButton>
|
</ActionSheet>
|
||||||
</ActionBar>
|
<ActionBarButton icon="arrow-double-left" @click="offsetRequest(85, false)"></ActionBarButton>
|
||||||
|
<ActionBarButton icon="arrow-left" @click="offsetRequest(10, false)"></ActionBarButton>
|
||||||
|
<ActionBarButton ref="playButton" :icon="playerStateStore.playerState.is_playing ? 'pause' : 'play'"
|
||||||
|
@click="playPauseRequest()">
|
||||||
|
</ActionBarButton>
|
||||||
|
<ActionBarButton icon="arrow" @click="offsetRequest(10, true)"></ActionBarButton>
|
||||||
|
<ActionBarButton icon="arrow-double-right" @click="offsetRequest(85, true)"></ActionBarButton>
|
||||||
|
</ActionBar>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#playerProgress {
|
||||||
|
position: fixed;
|
||||||
|
bottom: var(--van-action-bar-height);
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playerControls {
|
||||||
|
background-color: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-action-bar-button {
|
||||||
|
color: var(--color-primary-darker) !important;
|
||||||
|
}
|
||||||
|
</style>
|
@ -5,12 +5,15 @@ import { createPinia } from 'pinia'
|
|||||||
import Vant from 'vant';
|
import Vant from 'vant';
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import Qui from '@qvant/qui-max';
|
||||||
import 'vant/lib/index.css';
|
import 'vant/lib/index.css';
|
||||||
|
import '@qvant/qui-max/styles';
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
app.use(Vant)
|
app.use(Vant)
|
||||||
|
app.use(Qui)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
@ -2,24 +2,13 @@ import { defineStore } from "pinia";
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
export const useBackendStateStore = defineStore('backendState', () => {
|
export const useBackendStateStore = defineStore('backendState', () => {
|
||||||
const backendState = ref({
|
const backendState = ref({canWatch: false})
|
||||||
canWatch: false,
|
|
||||||
playing: false
|
|
||||||
})
|
|
||||||
|
|
||||||
async function updateCanWatch() {
|
async function updateCanWatch() {
|
||||||
const response = await fetch("/api/can-start-watching");
|
const response = await fetch("/api/commands/can-start-watching");
|
||||||
backendState.value.canWatch = response.ok;
|
backendState.value.canWatch = response.ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function updatePlaying() {
|
return { backendState, updateCanWatch }
|
||||||
const response = await fetch('/api/player/playing');
|
|
||||||
if (response.ok) {
|
|
||||||
let resp_json = await response.json()
|
|
||||||
backendState.value.playing = resp_json.playing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { backendState, updateCanWatch, updatePlaying }
|
|
||||||
});
|
});
|
67
frontend/src/stores/playersState.js
Normal file
67
frontend/src/stores/playersState.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export const usePlayerStateStore = defineStore('playerState', () => {
|
||||||
|
const playerState = ref({
|
||||||
|
players: [],
|
||||||
|
current_player: null,
|
||||||
|
is_playing: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
function updateIsPlaying() {
|
||||||
|
var is_playing = false
|
||||||
|
if (playerState.value.current_player == null) {
|
||||||
|
is_playing = false
|
||||||
|
}
|
||||||
|
for (let player of playerState.value.players) {
|
||||||
|
if (player.name == playerState.value.current_player && player.playing) {
|
||||||
|
is_playing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
playerState.value.is_playing = is_playing
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixCurrentPlayer() {
|
||||||
|
if (playerState.value.current_player == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let exists = false;
|
||||||
|
for (let player of playerState.value.players) {
|
||||||
|
if (player.name == playerState.value.current_player) {
|
||||||
|
exists = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!exists) {
|
||||||
|
playerState.value.current_player = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshPlayers() {
|
||||||
|
const response = await fetch("/api/player/list");
|
||||||
|
playerState.value.players = await response.json();
|
||||||
|
if (playerState.value.current_player == null && playerState.value.players.length > 0) {
|
||||||
|
playerState.value.current_player = playerState.value.players[0].name
|
||||||
|
}
|
||||||
|
fixCurrentPlayer()
|
||||||
|
updateIsPlaying()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCurrentPlayer(player) {
|
||||||
|
playerState.value.current_player = player
|
||||||
|
refreshPlayers()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentPlayer() {
|
||||||
|
if (playerState.value.current_player == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
for (let player of playerState.value.players) {
|
||||||
|
if (player.name == playerState.value.current_player) {
|
||||||
|
return player
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { playerState, refreshPlayers, setCurrentPlayer, getCurrentPlayer }
|
||||||
|
});
|
@ -1,5 +1,4 @@
|
|||||||
import { showFailToast } from 'vant';
|
import { showFailToast } from 'vant';
|
||||||
import { usePlayingStore } from '@/stores/playing';
|
|
||||||
|
|
||||||
async function postRequest(url, data) {
|
async function postRequest(url, data) {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
|
@ -3,30 +3,59 @@ import PlayerComponent from '@/components/PlayerComponent.vue';
|
|||||||
import { Space, Button } from 'vant';
|
import { Space, Button } from 'vant';
|
||||||
import { postRequest } from '@/utils';
|
import { postRequest } from '@/utils';
|
||||||
import { useBackendStateStore } from '@/stores/backendState'
|
import { useBackendStateStore } from '@/stores/backendState'
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import { usePlayerStateStore } from '@/stores/playersState';
|
||||||
|
import { QButton } from '@qvant/qui-max'
|
||||||
|
|
||||||
|
|
||||||
const backendStateStore = useBackendStateStore()
|
const backendStateStore = useBackendStateStore()
|
||||||
|
const playerStateStore = usePlayerStateStore()
|
||||||
|
|
||||||
backendStateStore.updateCanWatch()
|
backendStateStore.updateCanWatch()
|
||||||
|
|
||||||
function kill(...names) {
|
function kill(...names) {
|
||||||
postRequest("/api/kill", { names }).then(() => {
|
postRequest("/api/commands/kill", { names })
|
||||||
setTimeout(() => backendStateStore.updatePlaying(), 500)
|
playerStateStore.refreshPlayers()
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startWatching() {
|
function startWatching() {
|
||||||
postRequest("/api/start-watching").then(() => {
|
postRequest("/api/commands/start-watching")
|
||||||
setTimeout(() => backendStateStore.updatePlaying(), 500)
|
playerStateStore.refreshPlayers()
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
backendStateStore.updateCanWatch()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Space fill direction="vertical" :size="20">
|
<div id="controls">
|
||||||
<Button type="warning" round size="large" @click="kill('mpv')">Убить MPV</Button>
|
<QButton class="btn-ctrl" @click="kill('mpv')" theme="secondary" :full-width="true">
|
||||||
<Button type="danger" round size="large" @click="kill('awatch', 'mpv')">Убить awatch</Button>
|
Следующая серия
|
||||||
<Button type="primary" round size="large" :disabled="!backendStateStore.backendState.canWatch"
|
</QButton>
|
||||||
@click="startWatching">Начать потребление анимы</Button>
|
<QButton class="btn-ctrl" @click="kill('awatch', 'mpv')" theme="secondary" :full-width="true">
|
||||||
</Space>
|
Закончить потребление
|
||||||
|
</QButton>
|
||||||
|
<QButton class="btn-ctrl" @click="startWatching()" theme="secondary"
|
||||||
|
:disabled="!backendStateStore.backendState.canWatch" :full-width="true">
|
||||||
|
Начать потребление
|
||||||
|
</QButton>
|
||||||
|
</div>
|
||||||
<PlayerComponent />
|
<PlayerComponent />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#controls {
|
||||||
|
position: relative;
|
||||||
|
top: calc(50% - var(--van-action-bar-height));
|
||||||
|
margin-top: calc(-50px - var(--van-action-bar-height));
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ctrl {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
height: 60px;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
}
|
||||||
|
</style>
|
@ -13,7 +13,8 @@ export default defineConfig({
|
|||||||
name: 'Anime',
|
name: 'Anime',
|
||||||
short_name: 'Anime',
|
short_name: 'Anime',
|
||||||
start_url: '/',
|
start_url: '/',
|
||||||
theme_color: '#000000',
|
theme_color: '#F0EFF4',
|
||||||
|
display: 'standalone',
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: '/pwa-192x192.jpg',
|
src: '/pwa-192x192.jpg',
|
||||||
|
16
poetry.lock
generated
16
poetry.lock
generated
@ -100,6 +100,20 @@ files = [
|
|||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dbus-python"
|
||||||
|
version = "1.3.2"
|
||||||
|
description = "Python bindings for libdbus"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "dbus-python-1.3.2.tar.gz", hash = "sha256:ad67819308618b5069537be237f8e68ca1c7fcc95ee4a121fe6845b1418248f8"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
doc = ["sphinx", "sphinx_rtd_theme"]
|
||||||
|
test = ["tap.py"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.110.3"
|
version = "0.110.3"
|
||||||
@ -823,4 +837,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "f9e9fd7090b241c75ccfdafcf03212d386197cfc54664a7055ec40446a970232"
|
content-hash = "7df0274edad5941808c3d440c2474581502824a071f087938d9b46be5e6f8097"
|
||||||
|
@ -11,6 +11,7 @@ python = "^3.11"
|
|||||||
fastapi = "^0.110.3"
|
fastapi = "^0.110.3"
|
||||||
typer = "^0.12.3"
|
typer = "^0.12.3"
|
||||||
uvicorn = { version = "^0.29.0", extras = ["standard"] }
|
uvicorn = { version = "^0.29.0", extras = ["standard"] }
|
||||||
|
dbus-python = "^1.3.2"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
anime = "anime.__main__:main"
|
anime = "anime.__main__:main"
|
||||||
|
Reference in New Issue
Block a user