Udded player progress, updated UI.

Signed-off-by: Pavel Kirilin <win10@list.ru>
This commit is contained in:
2024-05-12 11:59:00 +02:00
parent 1b9a6ee0da
commit 0ca224fe50
23 changed files with 597 additions and 169 deletions

View File

@ -6,8 +6,13 @@
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Anime</title>
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
<meta name="theme-color" content="#ffffff">
<link rel="mask-icon" href="/mask-icon.svg" color="#F0EFF4">
<meta name="theme-color" content="#F0EFF4">
<style>
body {
background-color: "#F0EFF4";
}
</style>
</head>
<body>

View File

@ -11,6 +11,8 @@
"format": "prettier --write src/"
},
"dependencies": {
"@qvant/qui-max": "^0.19.0",
"@vueuse/core": "^10.9.0",
"pinia": "^2.1.7",
"vant": "^4.8.11",
"vue": "^3.4.21",

View File

@ -5,6 +5,12 @@ settings:
excludeLinksFromLockfile: false
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:
specifier: ^2.1.7
version: 2.1.7(vue@3.4.26)
@ -1271,7 +1277,6 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.1
dev: true
/@babel/template@7.24.0:
resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==}
@ -1638,6 +1643,25 @@ packages:
resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==}
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):
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
engines: {node: '>= 10.0.0'}
@ -1882,6 +1906,10 @@ packages:
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
dev: true
/@types/web-bluetooth@0.0.20:
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
dev: false
/@ungap/structured-clone@1.2.0:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
dev: true
@ -2065,6 +2093,31 @@ packages:
/@vue/shared@3.4.26:
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):
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@ -2142,6 +2195,10 @@ packages:
is-shared-array-buffer: 1.0.3
dev: true
/async-validator@4.2.5:
resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
dev: false
/async@3.2.5:
resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
dev: true
@ -2312,6 +2369,10 @@ packages:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: true
/colord@2.9.3:
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
dev: false
/commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
dev: true
@ -2385,6 +2446,13 @@ packages:
is-data-view: 1.0.1
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:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
@ -2842,6 +2910,10 @@ packages:
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
dev: true
/focus-visible@5.2.0:
resolution: {integrity: sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==}
dev: false
/for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
dependencies:
@ -3375,6 +3447,10 @@ packages:
p-locate: 5.0.0
dev: true
/lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
dev: false
/lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
dev: true
@ -3489,6 +3565,11 @@ packages:
boolbase: 1.0.0
dev: true
/object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
dev: false
/object-inspect@1.13.1:
resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
dev: true
@ -3698,7 +3779,6 @@ packages:
/regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
dev: true
/regenerator-transform@0.15.2:
resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==}

View File

@ -1,6 +1,6 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white: #F0EFF4;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
@ -36,19 +36,6 @@
--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,

View File

@ -1,30 +1,127 @@
<script setup>
import { ActionBar, ActionBarButton } from 'vant';
import { ActionBar, ActionBarButton, ActionSheet, Progress } from 'vant';
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) {
await postRequest(`/api/player/offset`, {
offset,
forward,
player: playerStateStore.playerState.current_player
})
await backendStore.updatePlaying()
playerStateStore.refreshPlayers()
}
async function playPauseRequest() {
await postRequest(`/api/player/play-pause`)
await backendStore.updatePlaying()
await postRequest(`/api/player/play-pause`, {
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>
<template>
<ActionBar>
<ActionBarButton icon="arrow-double-left" @click="offsetRequest(85, false)"></ActionBarButton>
<ActionBarButton icon="arrow-left" @click="offsetRequest(10, false)"></ActionBarButton>
<ActionBarButton :icon="backendStore.backendState.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>
</template>
<div v-if="playerStateStore.playerState.players.length > 0">
<Progress id="playerProgress" v-if="hasProgress()" :percentage="getProgress()" :show-pivot="false" />
<ActionBar id="playerControls">
<ActionSheet v-model:show="showPlayerSelect" :actions="getPlayersForChooser()" cancel-text="Cancel"
description="Available players" @select="onPlayerSelect">
</ActionSheet>
<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>
<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>

View File

@ -5,12 +5,15 @@ import { createPinia } from 'pinia'
import Vant from 'vant';
import App from './App.vue'
import router from './router'
import Qui from '@qvant/qui-max';
import 'vant/lib/index.css';
import '@qvant/qui-max/styles';
const app = createApp(App)
app.use(createPinia())
app.use(Vant)
app.use(Qui)
app.use(router)
app.mount('#app')

View File

@ -2,24 +2,13 @@ import { defineStore } from "pinia";
import { ref } from 'vue'
export const useBackendStateStore = defineStore('backendState', () => {
const backendState = ref({
canWatch: false,
playing: false
})
const backendState = ref({canWatch: false})
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;
}
async function updatePlaying() {
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 }
return { backendState, updateCanWatch }
});

View 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 }
});

View File

@ -1,5 +1,4 @@
import { showFailToast } from 'vant';
import { usePlayingStore } from '@/stores/playing';
async function postRequest(url, data) {
const response = await fetch(url, {

View File

@ -3,30 +3,59 @@ import PlayerComponent from '@/components/PlayerComponent.vue';
import { Space, Button } from 'vant';
import { postRequest } from '@/utils';
import { useBackendStateStore } from '@/stores/backendState'
import { onMounted } from 'vue';
import { usePlayerStateStore } from '@/stores/playersState';
import { QButton } from '@qvant/qui-max'
const backendStateStore = useBackendStateStore()
const playerStateStore = usePlayerStateStore()
backendStateStore.updateCanWatch()
function kill(...names) {
postRequest("/api/kill", { names }).then(() => {
setTimeout(() => backendStateStore.updatePlaying(), 500)
})
postRequest("/api/commands/kill", { names })
playerStateStore.refreshPlayers()
}
function startWatching() {
postRequest("/api/start-watching").then(() => {
setTimeout(() => backendStateStore.updatePlaying(), 500)
})
postRequest("/api/commands/start-watching")
playerStateStore.refreshPlayers()
}
onMounted(() => {
backendStateStore.updateCanWatch()
})
</script>
<template>
<Space fill direction="vertical" :size="20">
<Button type="warning" round size="large" @click="kill('mpv')">Убить MPV</Button>
<Button type="danger" round size="large" @click="kill('awatch', 'mpv')">Убить awatch</Button>
<Button type="primary" round size="large" :disabled="!backendStateStore.backendState.canWatch"
@click="startWatching">Начать потребление анимы</Button>
</Space>
<div id="controls">
<QButton class="btn-ctrl" @click="kill('mpv')" theme="secondary" :full-width="true">
Следующая серия
</QButton>
<QButton class="btn-ctrl" @click="kill('awatch', 'mpv')" theme="secondary" :full-width="true">
Закончить потребление
</QButton>
<QButton class="btn-ctrl" @click="startWatching()" theme="secondary"
:disabled="!backendStateStore.backendState.canWatch" :full-width="true">
Начать потребление
</QButton>
</div>
<PlayerComponent />
</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>

View File

@ -13,7 +13,8 @@ export default defineConfig({
name: 'Anime',
short_name: 'Anime',
start_url: '/',
theme_color: '#000000',
theme_color: '#F0EFF4',
display: 'standalone',
icons: [
{
src: '/pwa-192x192.jpg',