generated from VLADIMIR/template_frontend
up
This commit is contained in:
Vendored
+4
-4
@@ -5,10 +5,10 @@
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Вечерний детектив</title>
|
<title>Вечерний детектив</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BIuqSbcS.js"></script>
|
<script type="module" crossorigin src="/assets/index-GBc-6ZcB.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/client-CT8bsOZT.js">
|
<link rel="modulepreload" crossorigin href="/assets/client-BPUTdqiR.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/light-DhgSQSdG.js">
|
<link rel="modulepreload" crossorigin href="/assets/light-CAboqKml.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/light-DW22zwJY.js">
|
<link rel="modulepreload" crossorigin href="/assets/light-p8ayFy8X.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CF62QB4P.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CF62QB4P.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+211
-10
@@ -277,6 +277,62 @@ service EveningDetectiveServer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rpc GetPlaylists(GetPlaylistsReq) returns (GetPlaylistsRsp) {
|
||||||
|
option (google.api.http) = { get: "/api/playlists" };
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
tags: "Музыка";
|
||||||
|
summary: "Получить плейлисты";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc CreatePlaylist(CreatePlaylistReq) returns (CreatePlaylistRsp) {
|
||||||
|
option (google.api.http) = { post: "/api/playlists" body: "*" };
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
tags: "Музыка";
|
||||||
|
summary: "Создать плейлист";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc UpdatePlaylist(UpdatePlaylistReq) returns (UpdatePlaylistRsp) {
|
||||||
|
option (google.api.http) = { put: "/api/playlists/{ID}" body: "*" };
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
tags: "Музыка";
|
||||||
|
summary: "Изменить плейлист";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc DeletePlaylist(DeletePlaylistReq) returns (DeletePlaylistRsp) {
|
||||||
|
option (google.api.http) = { delete: "/api/playlists/{ID}" };
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
tags: "Музыка";
|
||||||
|
summary: "Удалить плейлист";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc UploadPlaylistTrack(UploadPlaylistTrackReq) returns (UploadPlaylistTrackRsp) {
|
||||||
|
option (google.api.http) = { post: "/api/playlists/{playlistID}/tracks" body: "*" };
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
tags: "Музыка";
|
||||||
|
summary: "Загрузить MP3 в плейлист";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc DeletePlaylistTrack(DeletePlaylistTrackReq) returns (DeletePlaylistTrackRsp) {
|
||||||
|
option (google.api.http) = { delete: "/api/playlists/{playlistID}/tracks/{trackID}" };
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
tags: "Музыка";
|
||||||
|
summary: "Удалить MP3 из плейлиста";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc SetScenarioPlaylist(SetScenarioPlaylistReq) returns (SetScenarioPlaylistRsp) {
|
||||||
|
option (google.api.http) = { put: "/api/scenarios/{ID}/playlist" body: "*" };
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
tags: "Музыка";
|
||||||
|
summary: "Назначить плейлист сценарию";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
rpc AddScenario(AddScenarioReq) returns (AddScenarioRsp) {
|
rpc AddScenario(AddScenarioReq) returns (AddScenarioRsp) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
post: "/api/scenario"
|
post: "/api/scenario"
|
||||||
@@ -596,6 +652,31 @@ service EveningDetectiveServer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rpc GetGameMusic(GetGameMusicReq) returns (GetGameMusicRsp) {
|
||||||
|
option (google.api.http) = { get: "/api/games/{gameID}/music" };
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
tags: "Музыка";
|
||||||
|
summary: "Получить музыку игры";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc SetGameMusic(SetGameMusicReq) returns (SetGameMusicRsp) {
|
||||||
|
option (google.api.http) = { post: "/api/games/{gameID}/music" body: "*" };
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
tags: "Музыка";
|
||||||
|
summary: "Изменить музыку игры";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc AdvanceScreenMusic(AdvanceScreenMusicReq) returns (AdvanceScreenMusicRsp) {
|
||||||
|
option (google.api.http) = { post: "/api/screens/music/advance" body: "*" };
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
security: { security_requirement: {} }
|
||||||
|
tags: "Музыка";
|
||||||
|
summary: "Перейти к следующему треку экрана";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Привязать экран к игре. Требует авторизацию организатора, автора или администратора.
|
// Привязать экран к игре. Требует авторизацию организатора, автора или администратора.
|
||||||
rpc BindScreen(BindScreenReq) returns (BindScreenRsp) {
|
rpc BindScreen(BindScreenReq) returns (BindScreenRsp) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
@@ -688,18 +769,18 @@ service EveningDetectiveServer {
|
|||||||
rpc GetScreenPlan(GetScreenPlanReq) returns (GetScreenPlanRsp) {
|
rpc GetScreenPlan(GetScreenPlanReq) returns (GetScreenPlanRsp) {
|
||||||
option (google.api.http) = { get: "/api/games/{gameID}/screen-plan" };
|
option (google.api.http) = { get: "/api/games/{gameID}/screen-plan" };
|
||||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
tags: "Экраны";
|
tags : "Экраны";
|
||||||
summary: "Получить план экранов";
|
summary: "Получить план экранов";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
rpc SaveScreenPlan(SaveScreenPlanReq) returns (SaveScreenPlanRsp) {
|
rpc SaveScreenPlan(SaveScreenPlanReq) returns (SaveScreenPlanRsp) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
put: "/api/games/{gameID}/screen-plan"
|
put : "/api/games/{gameID}/screen-plan"
|
||||||
body: "*"
|
body: "*"
|
||||||
};
|
};
|
||||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
tags: "Экраны";
|
tags : "Экраны";
|
||||||
summary: "Сохранить план экранов";
|
summary: "Сохранить план экранов";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -710,7 +791,7 @@ service EveningDetectiveServer {
|
|||||||
body: "*"
|
body: "*"
|
||||||
};
|
};
|
||||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
tags: "Экраны";
|
tags : "Экраны";
|
||||||
summary: "Перейти на шаг плана экранов";
|
summary: "Перейти на шаг плана экранов";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1164,6 +1245,73 @@ message DownloadFileReq {
|
|||||||
string filename = 1;
|
string filename = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message PlaylistTrack {
|
||||||
|
int32 ID = 1;
|
||||||
|
string name = 2;
|
||||||
|
string filename = 3;
|
||||||
|
int32 position = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Playlist {
|
||||||
|
int32 ID = 1;
|
||||||
|
string name = 2;
|
||||||
|
repeated PlaylistTrack tracks = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPlaylistsReq {}
|
||||||
|
|
||||||
|
message GetPlaylistsRsp {
|
||||||
|
repeated Playlist playlists = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreatePlaylistReq {
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreatePlaylistRsp {
|
||||||
|
Playlist playlist = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdatePlaylistReq {
|
||||||
|
int32 ID = 1;
|
||||||
|
string name = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdatePlaylistRsp {
|
||||||
|
Playlist playlist = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeletePlaylistReq {
|
||||||
|
int32 ID = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeletePlaylistRsp {}
|
||||||
|
|
||||||
|
message UploadPlaylistTrackReq {
|
||||||
|
int32 playlistID = 1;
|
||||||
|
string name = 2;
|
||||||
|
string filename = 3;
|
||||||
|
bytes data = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UploadPlaylistTrackRsp {
|
||||||
|
Playlist playlist = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeletePlaylistTrackReq {
|
||||||
|
int32 playlistID = 1;
|
||||||
|
int32 trackID = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeletePlaylistTrackRsp {}
|
||||||
|
|
||||||
|
message SetScenarioPlaylistReq {
|
||||||
|
int32 ID = 1;
|
||||||
|
int32 playlistID = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetScenarioPlaylistRsp {}
|
||||||
|
|
||||||
message AddScenarioReq {
|
message AddScenarioReq {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
}
|
}
|
||||||
@@ -1206,6 +1354,7 @@ message Scenario {
|
|||||||
double average_rating = 11;
|
double average_rating = 11;
|
||||||
int32 reviews_count = 12;
|
int32 reviews_count = 12;
|
||||||
string difficulty = 13;
|
string difficulty = 13;
|
||||||
|
optional int32 music_playlist_id = 14 [json_name = "music_playlist_id"];
|
||||||
}
|
}
|
||||||
|
|
||||||
message Story {
|
message Story {
|
||||||
@@ -1487,6 +1636,7 @@ message ScreenState {
|
|||||||
int64 timer_remaining_seconds = 18;
|
int64 timer_remaining_seconds = 18;
|
||||||
ScreenTimerState timer_state = 19;
|
ScreenTimerState timer_state = 19;
|
||||||
string text = 20;
|
string text = 20;
|
||||||
|
MusicState music = 21;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateScreenSessionReq {}
|
message CreateScreenSessionReq {}
|
||||||
@@ -1513,6 +1663,57 @@ message GetGameScreensRsp {
|
|||||||
repeated ScreenState screens = 1;
|
repeated ScreenState screens = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum MusicPlaybackState {
|
||||||
|
MUSIC_PLAYBACK_UNSPECIFIED = 0;
|
||||||
|
MUSIC_PLAYBACK_STOPPED = 1;
|
||||||
|
MUSIC_PLAYBACK_PLAYING = 2;
|
||||||
|
MUSIC_PLAYBACK_PAUSED = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MusicState {
|
||||||
|
Playlist playlist = 1;
|
||||||
|
int32 track_index = 2;
|
||||||
|
int64 position_ms = 3;
|
||||||
|
int32 volume = 4;
|
||||||
|
bool loop = 5;
|
||||||
|
MusicPlaybackState playback = 6;
|
||||||
|
int64 state_version = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetGameMusicReq {
|
||||||
|
int32 gameID = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetGameMusicRsp {
|
||||||
|
repeated Playlist playlists = 1;
|
||||||
|
MusicState state = 2;
|
||||||
|
int32 scenario_playlist_id = 3;
|
||||||
|
int32 game_playlist_id = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetGameMusicReq {
|
||||||
|
int32 gameID = 1;
|
||||||
|
int32 playlistID = 2;
|
||||||
|
int32 track_index = 3;
|
||||||
|
int64 position_ms = 4;
|
||||||
|
int32 volume = 5;
|
||||||
|
bool loop = 6;
|
||||||
|
MusicPlaybackState playback = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetGameMusicRsp {
|
||||||
|
MusicState state = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AdvanceScreenMusicReq {
|
||||||
|
string screenID = 1;
|
||||||
|
string accessToken = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AdvanceScreenMusicRsp {
|
||||||
|
MusicState state = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message BindScreenReq {
|
message BindScreenReq {
|
||||||
string screenID = 1;
|
string screenID = 1;
|
||||||
string accessToken = 2;
|
string accessToken = 2;
|
||||||
@@ -1603,8 +1804,8 @@ message ScreenPlanStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message ScreenPlan {
|
message ScreenPlan {
|
||||||
repeated ScreenPlanStep steps = 1;
|
repeated ScreenPlanStep steps = 1;
|
||||||
int32 current_step = 2;
|
int32 current_step = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetScreenPlanReq {
|
message GetScreenPlanReq {
|
||||||
@@ -1616,9 +1817,9 @@ message GetScreenPlanRsp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message SaveScreenPlanReq {
|
message SaveScreenPlanReq {
|
||||||
int32 gameID = 1;
|
int32 gameID = 1;
|
||||||
repeated ScreenPlanStep steps = 2;
|
repeated ScreenPlanStep steps = 2;
|
||||||
int32 current_step = 3;
|
int32 current_step = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SaveScreenPlanRsp {
|
message SaveScreenPlanRsp {
|
||||||
@@ -1626,7 +1827,7 @@ message SaveScreenPlanRsp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message SetScreenPlanStepReq {
|
message SetScreenPlanStepReq {
|
||||||
int32 gameID = 1;
|
int32 gameID = 1;
|
||||||
int32 current_step = 2;
|
int32 current_step = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ export type ScreenCommand =
|
|||||||
| "SCREEN_COMMAND_TIMER_PAUSE"
|
| "SCREEN_COMMAND_TIMER_PAUSE"
|
||||||
| "SCREEN_COMMAND_TIMER_RESUME"
|
| "SCREEN_COMMAND_TIMER_RESUME"
|
||||||
| "SCREEN_COMMAND_TIMER_RESET";
|
| "SCREEN_COMMAND_TIMER_RESET";
|
||||||
|
export type MusicPlaybackState =
|
||||||
|
| "MUSIC_PLAYBACK_UNSPECIFIED"
|
||||||
|
| "MUSIC_PLAYBACK_STOPPED"
|
||||||
|
| "MUSIC_PLAYBACK_PLAYING"
|
||||||
|
| "MUSIC_PLAYBACK_PAUSED";
|
||||||
export type PingReq = {
|
export type PingReq = {
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -197,6 +202,77 @@ export type DownloadFileReq = {
|
|||||||
filename: string | undefined;
|
filename: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PlaylistTrack = {
|
||||||
|
ID: number | undefined;
|
||||||
|
name: string | undefined;
|
||||||
|
filename: string | undefined;
|
||||||
|
position: number | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Playlist = {
|
||||||
|
ID: number | undefined;
|
||||||
|
name: string | undefined;
|
||||||
|
tracks: PlaylistTrack[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetPlaylistsReq = {
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetPlaylistsRsp = {
|
||||||
|
playlists: Playlist[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreatePlaylistReq = {
|
||||||
|
name: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreatePlaylistRsp = {
|
||||||
|
playlist: Playlist | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdatePlaylistReq = {
|
||||||
|
ID: number | undefined;
|
||||||
|
name: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdatePlaylistRsp = {
|
||||||
|
playlist: Playlist | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeletePlaylistReq = {
|
||||||
|
ID: number | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeletePlaylistRsp = {
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UploadPlaylistTrackReq = {
|
||||||
|
playlistID: number | undefined;
|
||||||
|
name: string | undefined;
|
||||||
|
filename: string | undefined;
|
||||||
|
data: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UploadPlaylistTrackRsp = {
|
||||||
|
playlist: Playlist | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeletePlaylistTrackReq = {
|
||||||
|
playlistID: number | undefined;
|
||||||
|
trackID: number | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeletePlaylistTrackRsp = {
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SetScenarioPlaylistReq = {
|
||||||
|
ID: number | undefined;
|
||||||
|
playlistID: number | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SetScenarioPlaylistRsp = {
|
||||||
|
};
|
||||||
|
|
||||||
export type AddScenarioReq = {
|
export type AddScenarioReq = {
|
||||||
name: string | undefined;
|
name: string | undefined;
|
||||||
};
|
};
|
||||||
@@ -226,6 +302,7 @@ export type Scenario = {
|
|||||||
averageRating: number | undefined;
|
averageRating: number | undefined;
|
||||||
reviewsCount: number | undefined;
|
reviewsCount: number | undefined;
|
||||||
difficulty: string | undefined;
|
difficulty: string | undefined;
|
||||||
|
music_playlist_id?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Story = {
|
export type Story = {
|
||||||
@@ -504,6 +581,17 @@ export type ScreenState = {
|
|||||||
timerRemainingSeconds: number | undefined;
|
timerRemainingSeconds: number | undefined;
|
||||||
timerState: ScreenTimerState | undefined;
|
timerState: ScreenTimerState | undefined;
|
||||||
text: string | undefined;
|
text: string | undefined;
|
||||||
|
music: MusicState | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MusicState = {
|
||||||
|
playlist: Playlist | undefined;
|
||||||
|
trackIndex: number | undefined;
|
||||||
|
positionMs: number | undefined;
|
||||||
|
volume: number | undefined;
|
||||||
|
loop: boolean | undefined;
|
||||||
|
playback: MusicPlaybackState | undefined;
|
||||||
|
stateVersion: number | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateScreenSessionReq = {
|
export type CreateScreenSessionReq = {
|
||||||
@@ -531,6 +619,40 @@ export type GetGameScreensRsp = {
|
|||||||
screens: ScreenState[] | undefined;
|
screens: ScreenState[] | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetGameMusicReq = {
|
||||||
|
gameID: number | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetGameMusicRsp = {
|
||||||
|
playlists: Playlist[] | undefined;
|
||||||
|
state: MusicState | undefined;
|
||||||
|
scenarioPlaylistId: number | undefined;
|
||||||
|
gamePlaylistId: number | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SetGameMusicReq = {
|
||||||
|
gameID: number | undefined;
|
||||||
|
playlistID: number | undefined;
|
||||||
|
trackIndex: number | undefined;
|
||||||
|
positionMs: number | undefined;
|
||||||
|
volume: number | undefined;
|
||||||
|
loop: boolean | undefined;
|
||||||
|
playback: MusicPlaybackState | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SetGameMusicRsp = {
|
||||||
|
state: MusicState | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdvanceScreenMusicReq = {
|
||||||
|
screenID: string | undefined;
|
||||||
|
accessToken: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdvanceScreenMusicRsp = {
|
||||||
|
state: MusicState | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export type BindScreenReq = {
|
export type BindScreenReq = {
|
||||||
screenID: string | undefined;
|
screenID: string | undefined;
|
||||||
accessToken: string | undefined;
|
accessToken: string | undefined;
|
||||||
@@ -1013,6 +1135,13 @@ export interface EveningDetectiveServer {
|
|||||||
GetPermissions(request: GetPermissionsReq): Promise<GetPermissionsRsp>;
|
GetPermissions(request: GetPermissionsReq): Promise<GetPermissionsRsp>;
|
||||||
UploadFile(request: UploadFileReq): Promise<UploadFileRsp>;
|
UploadFile(request: UploadFileReq): Promise<UploadFileRsp>;
|
||||||
DownloadFile(request: DownloadFileReq): Promise<googleapi_HttpBody>;
|
DownloadFile(request: DownloadFileReq): Promise<googleapi_HttpBody>;
|
||||||
|
GetPlaylists(request: GetPlaylistsReq): Promise<GetPlaylistsRsp>;
|
||||||
|
CreatePlaylist(request: CreatePlaylistReq): Promise<CreatePlaylistRsp>;
|
||||||
|
UpdatePlaylist(request: UpdatePlaylistReq): Promise<UpdatePlaylistRsp>;
|
||||||
|
DeletePlaylist(request: DeletePlaylistReq): Promise<DeletePlaylistRsp>;
|
||||||
|
UploadPlaylistTrack(request: UploadPlaylistTrackReq): Promise<UploadPlaylistTrackRsp>;
|
||||||
|
DeletePlaylistTrack(request: DeletePlaylistTrackReq): Promise<DeletePlaylistTrackRsp>;
|
||||||
|
SetScenarioPlaylist(request: SetScenarioPlaylistReq): Promise<SetScenarioPlaylistRsp>;
|
||||||
AddScenario(request: AddScenarioReq): Promise<AddScenarioRsp>;
|
AddScenario(request: AddScenarioReq): Promise<AddScenarioRsp>;
|
||||||
GetMyScenarios(request: GetMyScenariosReq): Promise<GetMyScenariosRsp>;
|
GetMyScenarios(request: GetMyScenariosReq): Promise<GetMyScenariosRsp>;
|
||||||
GetScenariosCatalog(request: GetScenariosCatalogReq): Promise<GetScenariosCatalogRsp>;
|
GetScenariosCatalog(request: GetScenariosCatalogReq): Promise<GetScenariosCatalogRsp>;
|
||||||
@@ -1046,6 +1175,9 @@ export interface EveningDetectiveServer {
|
|||||||
// Получить состояние экрана по его токену. Вызывается самим экраном.
|
// Получить состояние экрана по его токену. Вызывается самим экраном.
|
||||||
GetScreenState(request: GetScreenStateReq): Promise<GetScreenStateRsp>;
|
GetScreenState(request: GetScreenStateReq): Promise<GetScreenStateRsp>;
|
||||||
GetGameScreens(request: GetGameScreensReq): Promise<GetGameScreensRsp>;
|
GetGameScreens(request: GetGameScreensReq): Promise<GetGameScreensRsp>;
|
||||||
|
GetGameMusic(request: GetGameMusicReq): Promise<GetGameMusicRsp>;
|
||||||
|
SetGameMusic(request: SetGameMusicReq): Promise<SetGameMusicRsp>;
|
||||||
|
AdvanceScreenMusic(request: AdvanceScreenMusicReq): Promise<AdvanceScreenMusicRsp>;
|
||||||
// Привязать экран к игре. Требует авторизацию организатора, автора или администратора.
|
// Привязать экран к игре. Требует авторизацию организатора, автора или администратора.
|
||||||
BindScreen(request: BindScreenReq): Promise<BindScreenRsp>;
|
BindScreen(request: BindScreenReq): Promise<BindScreenRsp>;
|
||||||
// Отвязать экран от игры. Требует авторизацию организатора, автора или администратора.
|
// Отвязать экран от игры. Требует авторизацию организатора, автора или администратора.
|
||||||
@@ -1477,6 +1609,143 @@ export function createEveningDetectiveServerClient(
|
|||||||
method: "DownloadFile",
|
method: "DownloadFile",
|
||||||
}) as Promise<googleapi_HttpBody>;
|
}) as Promise<googleapi_HttpBody>;
|
||||||
},
|
},
|
||||||
|
GetPlaylists(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
const path = `api/playlists`; // eslint-disable-line quotes
|
||||||
|
const body = null;
|
||||||
|
const queryParams: string[] = [];
|
||||||
|
let uri = path;
|
||||||
|
if (queryParams.length > 0) {
|
||||||
|
uri += `?${queryParams.join("&")}`
|
||||||
|
}
|
||||||
|
return handler({
|
||||||
|
path: uri,
|
||||||
|
method: "GET",
|
||||||
|
body,
|
||||||
|
}, {
|
||||||
|
service: "EveningDetectiveServer",
|
||||||
|
method: "GetPlaylists",
|
||||||
|
}) as Promise<GetPlaylistsRsp>;
|
||||||
|
},
|
||||||
|
CreatePlaylist(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
const path = `api/playlists`; // eslint-disable-line quotes
|
||||||
|
const body = JSON.stringify(request);
|
||||||
|
const queryParams: string[] = [];
|
||||||
|
let uri = path;
|
||||||
|
if (queryParams.length > 0) {
|
||||||
|
uri += `?${queryParams.join("&")}`
|
||||||
|
}
|
||||||
|
return handler({
|
||||||
|
path: uri,
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
}, {
|
||||||
|
service: "EveningDetectiveServer",
|
||||||
|
method: "CreatePlaylist",
|
||||||
|
}) as Promise<CreatePlaylistRsp>;
|
||||||
|
},
|
||||||
|
UpdatePlaylist(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
if (!request.ID) {
|
||||||
|
throw new Error("missing required field request.ID");
|
||||||
|
}
|
||||||
|
const path = `api/playlists/${request.ID}`; // eslint-disable-line quotes
|
||||||
|
const body = JSON.stringify(request);
|
||||||
|
const queryParams: string[] = [];
|
||||||
|
let uri = path;
|
||||||
|
if (queryParams.length > 0) {
|
||||||
|
uri += `?${queryParams.join("&")}`
|
||||||
|
}
|
||||||
|
return handler({
|
||||||
|
path: uri,
|
||||||
|
method: "PUT",
|
||||||
|
body,
|
||||||
|
}, {
|
||||||
|
service: "EveningDetectiveServer",
|
||||||
|
method: "UpdatePlaylist",
|
||||||
|
}) as Promise<UpdatePlaylistRsp>;
|
||||||
|
},
|
||||||
|
DeletePlaylist(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
if (!request.ID) {
|
||||||
|
throw new Error("missing required field request.ID");
|
||||||
|
}
|
||||||
|
const path = `api/playlists/${request.ID}`; // eslint-disable-line quotes
|
||||||
|
const body = null;
|
||||||
|
const queryParams: string[] = [];
|
||||||
|
let uri = path;
|
||||||
|
if (queryParams.length > 0) {
|
||||||
|
uri += `?${queryParams.join("&")}`
|
||||||
|
}
|
||||||
|
return handler({
|
||||||
|
path: uri,
|
||||||
|
method: "DELETE",
|
||||||
|
body,
|
||||||
|
}, {
|
||||||
|
service: "EveningDetectiveServer",
|
||||||
|
method: "DeletePlaylist",
|
||||||
|
}) as Promise<DeletePlaylistRsp>;
|
||||||
|
},
|
||||||
|
UploadPlaylistTrack(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
if (!request.playlistID) {
|
||||||
|
throw new Error("missing required field request.playlistID");
|
||||||
|
}
|
||||||
|
const path = `api/playlists/${request.playlistID}/tracks`; // eslint-disable-line quotes
|
||||||
|
const body = JSON.stringify(request);
|
||||||
|
const queryParams: string[] = [];
|
||||||
|
let uri = path;
|
||||||
|
if (queryParams.length > 0) {
|
||||||
|
uri += `?${queryParams.join("&")}`
|
||||||
|
}
|
||||||
|
return handler({
|
||||||
|
path: uri,
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
}, {
|
||||||
|
service: "EveningDetectiveServer",
|
||||||
|
method: "UploadPlaylistTrack",
|
||||||
|
}) as Promise<UploadPlaylistTrackRsp>;
|
||||||
|
},
|
||||||
|
DeletePlaylistTrack(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
if (!request.playlistID) {
|
||||||
|
throw new Error("missing required field request.playlistID");
|
||||||
|
}
|
||||||
|
if (!request.trackID) {
|
||||||
|
throw new Error("missing required field request.trackID");
|
||||||
|
}
|
||||||
|
const path = `api/playlists/${request.playlistID}/tracks/${request.trackID}`; // eslint-disable-line quotes
|
||||||
|
const body = null;
|
||||||
|
const queryParams: string[] = [];
|
||||||
|
let uri = path;
|
||||||
|
if (queryParams.length > 0) {
|
||||||
|
uri += `?${queryParams.join("&")}`
|
||||||
|
}
|
||||||
|
return handler({
|
||||||
|
path: uri,
|
||||||
|
method: "DELETE",
|
||||||
|
body,
|
||||||
|
}, {
|
||||||
|
service: "EveningDetectiveServer",
|
||||||
|
method: "DeletePlaylistTrack",
|
||||||
|
}) as Promise<DeletePlaylistTrackRsp>;
|
||||||
|
},
|
||||||
|
SetScenarioPlaylist(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
if (!request.ID) {
|
||||||
|
throw new Error("missing required field request.ID");
|
||||||
|
}
|
||||||
|
const path = `api/scenarios/${request.ID}/playlist`; // eslint-disable-line quotes
|
||||||
|
const body = JSON.stringify(request);
|
||||||
|
const queryParams: string[] = [];
|
||||||
|
let uri = path;
|
||||||
|
if (queryParams.length > 0) {
|
||||||
|
uri += `?${queryParams.join("&")}`
|
||||||
|
}
|
||||||
|
return handler({
|
||||||
|
path: uri,
|
||||||
|
method: "PUT",
|
||||||
|
body,
|
||||||
|
}, {
|
||||||
|
service: "EveningDetectiveServer",
|
||||||
|
method: "SetScenarioPlaylist",
|
||||||
|
}) as Promise<SetScenarioPlaylistRsp>;
|
||||||
|
},
|
||||||
AddScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
AddScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
const path = `api/scenario`; // eslint-disable-line quotes
|
const path = `api/scenario`; // eslint-disable-line quotes
|
||||||
const body = JSON.stringify(request);
|
const body = JSON.stringify(request);
|
||||||
@@ -2045,6 +2314,63 @@ export function createEveningDetectiveServerClient(
|
|||||||
method: "GetGameScreens",
|
method: "GetGameScreens",
|
||||||
}) as Promise<GetGameScreensRsp>;
|
}) as Promise<GetGameScreensRsp>;
|
||||||
},
|
},
|
||||||
|
GetGameMusic(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
if (!request.gameID) {
|
||||||
|
throw new Error("missing required field request.gameID");
|
||||||
|
}
|
||||||
|
const path = `api/games/${request.gameID}/music`; // eslint-disable-line quotes
|
||||||
|
const body = null;
|
||||||
|
const queryParams: string[] = [];
|
||||||
|
let uri = path;
|
||||||
|
if (queryParams.length > 0) {
|
||||||
|
uri += `?${queryParams.join("&")}`
|
||||||
|
}
|
||||||
|
return handler({
|
||||||
|
path: uri,
|
||||||
|
method: "GET",
|
||||||
|
body,
|
||||||
|
}, {
|
||||||
|
service: "EveningDetectiveServer",
|
||||||
|
method: "GetGameMusic",
|
||||||
|
}) as Promise<GetGameMusicRsp>;
|
||||||
|
},
|
||||||
|
SetGameMusic(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
if (!request.gameID) {
|
||||||
|
throw new Error("missing required field request.gameID");
|
||||||
|
}
|
||||||
|
const path = `api/games/${request.gameID}/music`; // eslint-disable-line quotes
|
||||||
|
const body = JSON.stringify(request);
|
||||||
|
const queryParams: string[] = [];
|
||||||
|
let uri = path;
|
||||||
|
if (queryParams.length > 0) {
|
||||||
|
uri += `?${queryParams.join("&")}`
|
||||||
|
}
|
||||||
|
return handler({
|
||||||
|
path: uri,
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
}, {
|
||||||
|
service: "EveningDetectiveServer",
|
||||||
|
method: "SetGameMusic",
|
||||||
|
}) as Promise<SetGameMusicRsp>;
|
||||||
|
},
|
||||||
|
AdvanceScreenMusic(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
const path = `api/screens/music/advance`; // eslint-disable-line quotes
|
||||||
|
const body = JSON.stringify(request);
|
||||||
|
const queryParams: string[] = [];
|
||||||
|
let uri = path;
|
||||||
|
if (queryParams.length > 0) {
|
||||||
|
uri += `?${queryParams.join("&")}`
|
||||||
|
}
|
||||||
|
return handler({
|
||||||
|
path: uri,
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
}, {
|
||||||
|
service: "EveningDetectiveServer",
|
||||||
|
method: "AdvanceScreenMusic",
|
||||||
|
}) as Promise<AdvanceScreenMusicRsp>;
|
||||||
|
},
|
||||||
BindScreen(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
BindScreen(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
if (!request.screenID) {
|
if (!request.screenID) {
|
||||||
throw new Error("missing required field request.screenID");
|
throw new Error("missing required field request.screenID");
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ import type {
|
|||||||
Game,
|
Game,
|
||||||
GameReview,
|
GameReview,
|
||||||
MapItem,
|
MapItem,
|
||||||
|
MusicPlaybackState,
|
||||||
|
MusicState,
|
||||||
|
Playlist,
|
||||||
Question,
|
Question,
|
||||||
ResultsQuestion,
|
ResultsQuestion,
|
||||||
ScreenCommand,
|
ScreenCommand,
|
||||||
@@ -114,6 +117,8 @@ const activeTeamCount = computed(
|
|||||||
() => (game.value.teams || []).filter((team) => team.participationStatus !== 'cancelled').length,
|
() => (game.value.teams || []).filter((team) => team.participationStatus !== 'cancelled').length,
|
||||||
)
|
)
|
||||||
const screens = ref<ScreenState[]>([])
|
const screens = ref<ScreenState[]>([])
|
||||||
|
const playlists = ref<Playlist[]>([])
|
||||||
|
const music = ref<MusicState>()
|
||||||
const screenPlan = ref<ScreenPlan>()
|
const screenPlan = ref<ScreenPlan>()
|
||||||
const screenPlanDraft = ref<ScreenPlanStep[]>([])
|
const screenPlanDraft = ref<ScreenPlanStep[]>([])
|
||||||
const screenPlanDirty = ref(false)
|
const screenPlanDirty = ref(false)
|
||||||
@@ -185,6 +190,17 @@ async function getGameScreens() {
|
|||||||
}
|
}
|
||||||
getGameScreens()
|
getGameScreens()
|
||||||
|
|
||||||
|
async function getGameMusic() {
|
||||||
|
try {
|
||||||
|
const res = await client.GetGameMusic({ gameID: Number(gameId) })
|
||||||
|
playlists.value = res.playlists || []
|
||||||
|
music.value = res.state
|
||||||
|
} catch (err) {
|
||||||
|
message.error(err instanceof Error ? err.message : 'Не удалось получить музыку игры')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getGameMusic()
|
||||||
|
|
||||||
function isScreenPlanEditing() {
|
function isScreenPlanEditing() {
|
||||||
return screenPlanMode.value === 'edit'
|
return screenPlanMode.value === 'edit'
|
||||||
}
|
}
|
||||||
@@ -360,6 +376,81 @@ async function setSoundScreen(screen: ScreenState) {
|
|||||||
await getGameScreens()
|
await getGameScreens()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setGameMusic(change: {
|
||||||
|
playlistID?: number
|
||||||
|
trackIndex?: number
|
||||||
|
positionMs?: number
|
||||||
|
playback?: MusicPlaybackState
|
||||||
|
loop?: boolean
|
||||||
|
volume?: number
|
||||||
|
}) {
|
||||||
|
const current = music.value
|
||||||
|
try {
|
||||||
|
const res = await client.SetGameMusic({
|
||||||
|
gameID: Number(gameId),
|
||||||
|
playlistID: change.playlistID ?? current?.playlist?.ID ?? 0,
|
||||||
|
trackIndex: change.trackIndex ?? current?.trackIndex ?? 0,
|
||||||
|
positionMs: change.positionMs ?? current?.positionMs ?? 0,
|
||||||
|
volume: change.volume ?? current?.volume ?? 100,
|
||||||
|
loop: change.loop ?? current?.loop ?? false,
|
||||||
|
playback: change.playback ?? current?.playback ?? 'MUSIC_PLAYBACK_STOPPED',
|
||||||
|
})
|
||||||
|
music.value = res.state
|
||||||
|
} catch (err) {
|
||||||
|
message.error(err instanceof Error ? err.message : 'Не удалось изменить музыку')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createPlaylist(name: string) {
|
||||||
|
try {
|
||||||
|
await client.CreatePlaylist({ name })
|
||||||
|
await getGameMusic()
|
||||||
|
} catch (err) {
|
||||||
|
message.error(err instanceof Error ? err.message : 'Не удалось создать плейлист')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePlaylist(id: number) {
|
||||||
|
try {
|
||||||
|
await client.DeletePlaylist({ ID: id })
|
||||||
|
await getGameMusic()
|
||||||
|
} catch (err) {
|
||||||
|
message.error(err instanceof Error ? err.message : 'Не удалось удалить плейлист')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadTrack(file: File) {
|
||||||
|
const playlistID = music.value?.playlist?.ID
|
||||||
|
if (!playlistID) {
|
||||||
|
message.warning('Сначала выберите плейлист')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = uint8ToBase64(new Uint8Array(await file.arrayBuffer()))
|
||||||
|
await client.UploadPlaylistTrack({ playlistID, name: file.name, filename: file.name, data })
|
||||||
|
await getGameMusic()
|
||||||
|
} catch (err) {
|
||||||
|
message.error(err instanceof Error ? err.message : 'Не удалось загрузить MP3')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTrack(id: number) {
|
||||||
|
const playlistID = music.value?.playlist?.ID
|
||||||
|
if (!playlistID) return
|
||||||
|
try {
|
||||||
|
await client.DeletePlaylistTrack({ playlistID, trackID: id })
|
||||||
|
await getGameMusic()
|
||||||
|
} catch (err) {
|
||||||
|
message.error(err instanceof Error ? err.message : 'Не удалось удалить трек')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function uint8ToBase64(data: Uint8Array): string {
|
||||||
|
let binary = ''
|
||||||
|
for (let i = 0; i < data.length; i += 0x8000) binary += String.fromCharCode(...data.subarray(i, i + 0x8000))
|
||||||
|
return btoa(binary)
|
||||||
|
}
|
||||||
|
|
||||||
async function unbindScreen(screen: ScreenState) {
|
async function unbindScreen(screen: ScreenState) {
|
||||||
await client.UnbindScreen({ screenID: screen.ID })
|
await client.UnbindScreen({ screenID: screen.ID })
|
||||||
await getGameScreens()
|
await getGameScreens()
|
||||||
@@ -539,6 +630,7 @@ async function issueTickets() {
|
|||||||
useSimplePolling(() => {
|
useSimplePolling(() => {
|
||||||
getGame(Number(gameId))
|
getGame(Number(gameId))
|
||||||
getGameScreens()
|
getGameScreens()
|
||||||
|
getGameMusic()
|
||||||
getScreenPlan()
|
getScreenPlan()
|
||||||
getGameMap()
|
getGameMap()
|
||||||
}, 2000)
|
}, 2000)
|
||||||
@@ -811,9 +903,16 @@ useSimplePolling(() => {
|
|||||||
<GameScreensPanel
|
<GameScreensPanel
|
||||||
:screens="screens"
|
:screens="screens"
|
||||||
:screen-binding="screenBinding"
|
:screen-binding="screenBinding"
|
||||||
|
:playlists="playlists"
|
||||||
|
:music="music"
|
||||||
@bind="bindScannedScreen"
|
@bind="bindScannedScreen"
|
||||||
@set-sound="setSoundScreen"
|
@set-sound="setSoundScreen"
|
||||||
@unbind="unbindScreen"
|
@unbind="unbindScreen"
|
||||||
|
@set-music="setGameMusic"
|
||||||
|
@create-playlist="createPlaylist"
|
||||||
|
@delete-playlist="deletePlaylist"
|
||||||
|
@upload-track="uploadTrack"
|
||||||
|
@delete-track="deleteTrack"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|||||||
@@ -1,23 +1,35 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NAlert, NButton, NFlex, NTag } from 'naive-ui'
|
import { NAlert, NButton, NFlex, NInput, NInputNumber, NSelect, NSwitch, NTag, NUpload, type UploadFileInfo } from 'naive-ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import type { ScreenState } from '@/api/generated/crabs/evening_detective_server'
|
import type { MusicState, Playlist, ScreenState } from '@/api/generated/crabs/evening_detective_server'
|
||||||
import ScreenBindingModal from '@/components/ScreenBindingModal.vue'
|
import ScreenBindingModal from '@/components/ScreenBindingModal.vue'
|
||||||
import UiBlock from '@/components/UiBlock.vue'
|
import UiBlock from '@/components/UiBlock.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
screens: ScreenState[]
|
screens: ScreenState[]
|
||||||
screenBinding: boolean
|
screenBinding: boolean
|
||||||
|
playlists: Playlist[]
|
||||||
|
music?: MusicState
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
bind: [screenID: string, accessToken: string]
|
bind: [screenID: string, accessToken: string]
|
||||||
setSound: [screen: ScreenState]
|
setSound: [screen: ScreenState]
|
||||||
unbind: [screen: ScreenState]
|
unbind: [screen: ScreenState]
|
||||||
|
setMusic: [change: { playlistID?: number; trackIndex?: number; positionMs?: number; playback?: MusicState['playback']; loop?: boolean; volume?: number }]
|
||||||
|
createPlaylist: [name: string]
|
||||||
|
deletePlaylist: [id: number]
|
||||||
|
uploadTrack: [file: File]
|
||||||
|
deleteTrack: [id: number]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const showBindingModal = ref(false)
|
const showBindingModal = ref(false)
|
||||||
|
const playlistName = ref('')
|
||||||
|
|
||||||
|
function uploadTrack(options: { file: UploadFileInfo }) {
|
||||||
|
if (options.file.file) emit('uploadTrack', options.file.file)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -58,6 +70,79 @@ const showBindingModal = ref(false)
|
|||||||
:binding="props.screenBinding"
|
:binding="props.screenBinding"
|
||||||
@scanned="(screenID, accessToken) => emit('bind', screenID, accessToken)"
|
@scanned="(screenID, accessToken) => emit('bind', screenID, accessToken)"
|
||||||
/>
|
/>
|
||||||
|
<UiBlock class="music-block">
|
||||||
|
<p class="screens-title">Музыка</p>
|
||||||
|
<n-flex align="center" wrap class="playlist-actions">
|
||||||
|
<n-input v-model:value="playlistName" placeholder="Новый плейлист" style="max-width: 220px" />
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
:disabled="!playlistName.trim()"
|
||||||
|
@click="emit('createPlaylist', playlistName.trim()); playlistName = ''"
|
||||||
|
>
|
||||||
|
Создать
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
v-if="props.music?.playlist?.ID"
|
||||||
|
size="small"
|
||||||
|
type="error"
|
||||||
|
ghost
|
||||||
|
@click="emit('deletePlaylist', props.music.playlist.ID)"
|
||||||
|
>
|
||||||
|
Удалить плейлист
|
||||||
|
</n-button>
|
||||||
|
<n-upload accept=".mp3,audio/mpeg" :default-upload="false" :show-file-list="false" @change="uploadTrack">
|
||||||
|
<n-button size="small">Добавить MP3</n-button>
|
||||||
|
</n-upload>
|
||||||
|
</n-flex>
|
||||||
|
<n-flex align="center" wrap>
|
||||||
|
<n-select
|
||||||
|
:value="props.music?.playlist?.ID"
|
||||||
|
:options="props.playlists.map((playlist) => ({ label: playlist.name, value: playlist.ID }))"
|
||||||
|
placeholder="Плейлист"
|
||||||
|
clearable
|
||||||
|
style="min-width: 220px"
|
||||||
|
@update:value="(value) => emit('setMusic', { playlistID: value || 0, trackIndex: 0, playback: 'MUSIC_PLAYBACK_STOPPED' })"
|
||||||
|
/>
|
||||||
|
<n-button
|
||||||
|
v-if="props.music?.playlist?.tracks?.[props.music.trackIndex || 0]?.ID"
|
||||||
|
size="small"
|
||||||
|
type="error"
|
||||||
|
ghost
|
||||||
|
@click="emit('deleteTrack', props.music.playlist.tracks[props.music.trackIndex || 0].ID!)"
|
||||||
|
>
|
||||||
|
Удалить трек
|
||||||
|
</n-button>
|
||||||
|
<n-select
|
||||||
|
v-if="props.music?.playlist?.tracks?.length"
|
||||||
|
:value="props.music.trackIndex"
|
||||||
|
:options="props.music.playlist.tracks.map((track, index) => ({ label: track.name || track.filename || `Трек ${index + 1}`, value: index }))"
|
||||||
|
style="min-width: 220px"
|
||||||
|
@update:value="(value) => emit('setMusic', { trackIndex: value })"
|
||||||
|
/>
|
||||||
|
<n-button size="small" @click="emit('setMusic', { playback: 'MUSIC_PLAYBACK_PLAYING' })">Старт</n-button>
|
||||||
|
<n-button size="small" @click="emit('setMusic', { playback: 'MUSIC_PLAYBACK_PAUSED' })">Пауза</n-button>
|
||||||
|
<n-button size="small" @click="emit('setMusic', { playback: 'MUSIC_PLAYBACK_STOPPED', positionMs: 0 })">Стоп</n-button>
|
||||||
|
<n-input-number
|
||||||
|
:value="props.music?.volume"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
:step="5"
|
||||||
|
size="small"
|
||||||
|
style="width: 110px"
|
||||||
|
@update:value="(value) => value != null && emit('setMusic', { volume: value })"
|
||||||
|
>
|
||||||
|
<template #prefix>Громкость</template>
|
||||||
|
</n-input-number>
|
||||||
|
<n-switch
|
||||||
|
:value="props.music?.loop"
|
||||||
|
:round="false"
|
||||||
|
@update:value="(value) => emit('setMusic', { loop: value })"
|
||||||
|
>
|
||||||
|
<template #checked>Loop</template>
|
||||||
|
<template #unchecked>Loop</template>
|
||||||
|
</n-switch>
|
||||||
|
</n-flex>
|
||||||
|
</UiBlock>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -71,6 +156,14 @@ const showBindingModal = ref(false)
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.music-block {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-actions {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.screen-row {
|
.screen-row {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-top: 1px solid rgb(255 255 255 / 10%);
|
border-top: 1px solid rgb(255 255 255 / 10%);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
NFlex,
|
NFlex,
|
||||||
NInput,
|
NInput,
|
||||||
NModal,
|
NModal,
|
||||||
|
NSelect,
|
||||||
NSpace,
|
NSpace,
|
||||||
NTag,
|
NTag,
|
||||||
NText,
|
NText,
|
||||||
@@ -19,7 +20,7 @@ import { useRoute } from 'vue-router'
|
|||||||
|
|
||||||
import { getAuthClient } from '@/api/auth_client'
|
import { getAuthClient } from '@/api/auth_client'
|
||||||
import { getBinaryClient } from '@/api/binary_client'
|
import { getBinaryClient } from '@/api/binary_client'
|
||||||
import type { Conclusion, Scenario } from '@/api/generated/crabs/evening_detective_server'
|
import type { Conclusion, Playlist, Scenario } from '@/api/generated/crabs/evening_detective_server'
|
||||||
import HeaderMenu from '@/components/HeaderMenu.vue'
|
import HeaderMenu from '@/components/HeaderMenu.vue'
|
||||||
import IntroBlock from '@/components/IntroBlock.vue'
|
import IntroBlock from '@/components/IntroBlock.vue'
|
||||||
import PlaceBlock from '@/components/PlaceBlock.vue'
|
import PlaceBlock from '@/components/PlaceBlock.vue'
|
||||||
@@ -72,6 +73,8 @@ const conclusion = ref<Conclusion>({ text: '', video: '' })
|
|||||||
const conclusionFiles = ref<UploadFileInfo[]>([])
|
const conclusionFiles = ref<UploadFileInfo[]>([])
|
||||||
const savingConclusion = ref(false)
|
const savingConclusion = ref(false)
|
||||||
const conclusionVideoURL = computed(() => applicationFileURL(conclusion.value.video))
|
const conclusionVideoURL = computed(() => applicationFileURL(conclusion.value.video))
|
||||||
|
const playlists = ref<Playlist[]>([])
|
||||||
|
const selectedPlaylistID = ref<number | null>(null)
|
||||||
|
|
||||||
function resetConclusion() {
|
function resetConclusion() {
|
||||||
conclusion.value = { text: '', video: '', ...scenario.value.story?.conclusion }
|
conclusion.value = { text: '', video: '', ...scenario.value.story?.conclusion }
|
||||||
@@ -146,6 +149,7 @@ const editorQuestions = computed<EditableQuestion[]>(() => {
|
|||||||
async function getScenario(id: number) {
|
async function getScenario(id: number) {
|
||||||
const res = await client.GetFullScenario({ ID: id })
|
const res = await client.GetFullScenario({ ID: id })
|
||||||
scenario.value = res.scenario!
|
scenario.value = res.scenario!
|
||||||
|
selectedPlaylistID.value = scenario.value.music_playlist_id || null
|
||||||
resetConclusion()
|
resetConclusion()
|
||||||
scenario.value.story?.places?.forEach((item) => {
|
scenario.value.story?.places?.forEach((item) => {
|
||||||
const editable = item as EditablePlace
|
const editable = item as EditablePlace
|
||||||
@@ -162,6 +166,25 @@ async function getScenario(id: number) {
|
|||||||
}
|
}
|
||||||
getScenario(Number(scenarioId))
|
getScenario(Number(scenarioId))
|
||||||
|
|
||||||
|
async function loadPlaylists() {
|
||||||
|
try {
|
||||||
|
const res = await client.GetPlaylists({})
|
||||||
|
playlists.value = res.playlists || []
|
||||||
|
} catch (err) {
|
||||||
|
message.error(err instanceof Error ? err.message : 'Не удалось загрузить плейлисты')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadPlaylists()
|
||||||
|
|
||||||
|
async function saveScenarioPlaylist(value: number | null) {
|
||||||
|
try {
|
||||||
|
await client.SetScenarioPlaylist({ ID: Number(scenarioId), playlistID: value || 0 })
|
||||||
|
message.success('Плейлист сценария сохранён')
|
||||||
|
} catch (err) {
|
||||||
|
message.error(err instanceof Error ? err.message : 'Не удалось сохранить плейлист')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteScenario(id: number) {
|
async function deleteScenario(id: number) {
|
||||||
await client.DeleteScenario({ ID: id })
|
await client.DeleteScenario({ ID: id })
|
||||||
router.push('/scenarios')
|
router.push('/scenarios')
|
||||||
@@ -584,6 +607,16 @@ function getRandomComplimentForNastya() {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<n-card title="Музыка сценария">
|
||||||
|
<n-select
|
||||||
|
v-model:value="selectedPlaylistID"
|
||||||
|
:options="playlists.map((playlist) => ({ label: playlist.name, value: playlist.ID }))"
|
||||||
|
clearable
|
||||||
|
placeholder="Плейлист по умолчанию"
|
||||||
|
@update:value="saveScenarioPlaylist"
|
||||||
|
/>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
<n-card title="Фон дела">
|
<n-card title="Фон дела">
|
||||||
<n-space vertical>
|
<n-space vertical>
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const timerSyncedAt = ref(Date.now())
|
|||||||
const timerStateAtSync = ref<string>()
|
const timerStateAtSync = ref<string>()
|
||||||
const pollingReady = ref(false)
|
const pollingReady = ref(false)
|
||||||
const introAudioElement = ref<HTMLAudioElement>()
|
const introAudioElement = ref<HTMLAudioElement>()
|
||||||
|
const musicAudioElement = ref<HTMLAudioElement>()
|
||||||
const conclusionVideoElement = ref<HTMLVideoElement>()
|
const conclusionVideoElement = ref<HTMLVideoElement>()
|
||||||
let appliedCommandVersion = 0
|
let appliedCommandVersion = 0
|
||||||
let timerTick: number | null = null
|
let timerTick: number | null = null
|
||||||
@@ -68,6 +69,11 @@ const timerDisplay = computed(() => {
|
|||||||
})
|
})
|
||||||
const introAudio = computed(() => applicationFileURL(state.value?.introAudio))
|
const introAudio = computed(() => applicationFileURL(state.value?.introAudio))
|
||||||
const conclusionVideo = computed(() => applicationFileURL(state.value?.conclusionVideo))
|
const conclusionVideo = computed(() => applicationFileURL(state.value?.conclusionVideo))
|
||||||
|
const musicTrack = computed(() => {
|
||||||
|
const tracks = state.value?.music?.playlist?.tracks || []
|
||||||
|
const index = state.value?.music?.trackIndex || 0
|
||||||
|
return applicationFileURL(tracks[index]?.filename)
|
||||||
|
})
|
||||||
const screenBackground = computed(() => {
|
const screenBackground = computed(() => {
|
||||||
const image = state.value?.backgroundImage || state.value?.coverImage
|
const image = state.value?.backgroundImage || state.value?.coverImage
|
||||||
return image ? { backgroundImage: `linear-gradient(rgb(0 0 0 / 55%), rgb(0 0 0 / 55%)), url("${image}")` } : {}
|
return image ? { backgroundImage: `linear-gradient(rgb(0 0 0 / 55%), rgb(0 0 0 / 55%)), url("${image}")` } : {}
|
||||||
@@ -188,6 +194,35 @@ watch(state, async (value) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => state.value?.music,
|
||||||
|
async (value) => {
|
||||||
|
const media = musicAudioElement.value
|
||||||
|
if (!media || !value || !value.playlist?.tracks?.length) return
|
||||||
|
await nextTick()
|
||||||
|
media.volume = Math.max(0, Math.min(1, (value.volume ?? 100) / 100))
|
||||||
|
media.muted = !state.value?.soundEnabled
|
||||||
|
media.currentTime = (value.positionMs || 0) / 1000
|
||||||
|
if (value.playback === 'MUSIC_PLAYBACK_PLAYING') {
|
||||||
|
void media.play().catch(() => {
|
||||||
|
mediaError.value = 'Браузер заблокировал воспроизведение музыки. Нажмите «Подготовить экран».'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
media.pause()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
async function advanceMusic() {
|
||||||
|
if (!screenId.value || !accessToken.value) return
|
||||||
|
try {
|
||||||
|
await screenClient.AdvanceScreenMusic({ screenID: screenId.value, accessToken: accessToken.value })
|
||||||
|
} catch {
|
||||||
|
mediaError.value = 'Не удалось синхронизировать музыку. Повторная попытка выполняется автоматически.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function bind() {
|
async function bind() {
|
||||||
if (!selectedGameId.value || !route.query.screenID || !route.query.accessToken) return
|
if (!selectedGameId.value || !route.query.screenID || !route.query.accessToken) return
|
||||||
binding.value = true
|
binding.value = true
|
||||||
@@ -213,7 +248,7 @@ async function prepareScreen() {
|
|||||||
}
|
}
|
||||||
prepared.value = true
|
prepared.value = true
|
||||||
await nextTick()
|
await nextTick()
|
||||||
for (const media of [introAudioElement.value, conclusionVideoElement.value]) {
|
for (const media of [introAudioElement.value, conclusionVideoElement.value, musicAudioElement.value]) {
|
||||||
if (!media) continue
|
if (!media) continue
|
||||||
media.muted = true
|
media.muted = true
|
||||||
try {
|
try {
|
||||||
@@ -288,6 +323,13 @@ onUnmounted(() => {
|
|||||||
<div v-if="state.introAudio" v-show="screenMode === 'SCREEN_MODE_INTRO'" class="screen-media">
|
<div v-if="state.introAudio" v-show="screenMode === 'SCREEN_MODE_INTRO'" class="screen-media">
|
||||||
<audio ref="introAudioElement" :src="introAudio" :muted="!state.soundEnabled" controls />
|
<audio ref="introAudioElement" :src="introAudio" :muted="!state.soundEnabled" controls />
|
||||||
</div>
|
</div>
|
||||||
|
<audio
|
||||||
|
v-if="state.music?.playlist?.tracks?.length"
|
||||||
|
ref="musicAudioElement"
|
||||||
|
:src="musicTrack"
|
||||||
|
:muted="!state.soundEnabled"
|
||||||
|
@ended="advanceMusic"
|
||||||
|
/>
|
||||||
<div v-if="state.conclusionVideo" v-show="screenMode === 'SCREEN_MODE_CONCLUSION'" class="conclusion-screen">
|
<div v-if="state.conclusionVideo" v-show="screenMode === 'SCREEN_MODE_CONCLUSION'" class="conclusion-screen">
|
||||||
<video ref="conclusionVideoElement" :src="conclusionVideo" :muted="!state.soundEnabled" playsinline />
|
<video ref="conclusionVideoElement" :src="conclusionVideo" :muted="!state.soundEnabled" playsinline />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user