diff --git a/dist/index.html b/dist/index.html
index 69d121e..17caf80 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -5,10 +5,10 @@
Вечерний детектив
-
-
-
-
+
+
+
+
diff --git a/proto/main.proto b/proto/main.proto
index 31e76aa..9cf2a10 100644
--- a/proto/main.proto
+++ b/proto/main.proto
@@ -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) {
option (google.api.http) = {
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) {
option (google.api.http) = {
@@ -688,18 +769,18 @@ service EveningDetectiveServer {
rpc GetScreenPlan(GetScreenPlanReq) returns (GetScreenPlanRsp) {
option (google.api.http) = { get: "/api/games/{gameID}/screen-plan" };
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
- tags: "Экраны";
+ tags : "Экраны";
summary: "Получить план экранов";
};
}
rpc SaveScreenPlan(SaveScreenPlanReq) returns (SaveScreenPlanRsp) {
option (google.api.http) = {
- put: "/api/games/{gameID}/screen-plan"
+ put : "/api/games/{gameID}/screen-plan"
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
- tags: "Экраны";
+ tags : "Экраны";
summary: "Сохранить план экранов";
};
}
@@ -710,7 +791,7 @@ service EveningDetectiveServer {
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
- tags: "Экраны";
+ tags : "Экраны";
summary: "Перейти на шаг плана экранов";
};
}
@@ -1164,6 +1245,73 @@ message DownloadFileReq {
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 {
string name = 1;
}
@@ -1206,6 +1354,7 @@ message Scenario {
double average_rating = 11;
int32 reviews_count = 12;
string difficulty = 13;
+ optional int32 music_playlist_id = 14 [json_name = "music_playlist_id"];
}
message Story {
@@ -1487,6 +1636,7 @@ message ScreenState {
int64 timer_remaining_seconds = 18;
ScreenTimerState timer_state = 19;
string text = 20;
+ MusicState music = 21;
}
message CreateScreenSessionReq {}
@@ -1513,6 +1663,57 @@ message GetGameScreensRsp {
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 {
string screenID = 1;
string accessToken = 2;
@@ -1603,8 +1804,8 @@ message ScreenPlanStep {
}
message ScreenPlan {
- repeated ScreenPlanStep steps = 1;
- int32 current_step = 2;
+ repeated ScreenPlanStep steps = 1;
+ int32 current_step = 2;
}
message GetScreenPlanReq {
@@ -1616,9 +1817,9 @@ message GetScreenPlanRsp {
}
message SaveScreenPlanReq {
- int32 gameID = 1;
- repeated ScreenPlanStep steps = 2;
- int32 current_step = 3;
+ int32 gameID = 1;
+ repeated ScreenPlanStep steps = 2;
+ int32 current_step = 3;
}
message SaveScreenPlanRsp {
@@ -1626,7 +1827,7 @@ message SaveScreenPlanRsp {
}
message SetScreenPlanStepReq {
- int32 gameID = 1;
+ int32 gameID = 1;
int32 current_step = 2;
}
diff --git a/src/api/generated/crabs/evening_detective_server/index.ts b/src/api/generated/crabs/evening_detective_server/index.ts
index 5406cb5..bb93276 100644
--- a/src/api/generated/crabs/evening_detective_server/index.ts
+++ b/src/api/generated/crabs/evening_detective_server/index.ts
@@ -31,6 +31,11 @@ export type ScreenCommand =
| "SCREEN_COMMAND_TIMER_PAUSE"
| "SCREEN_COMMAND_TIMER_RESUME"
| "SCREEN_COMMAND_TIMER_RESET";
+export type MusicPlaybackState =
+ | "MUSIC_PLAYBACK_UNSPECIFIED"
+ | "MUSIC_PLAYBACK_STOPPED"
+ | "MUSIC_PLAYBACK_PLAYING"
+ | "MUSIC_PLAYBACK_PAUSED";
export type PingReq = {
};
@@ -197,6 +202,77 @@ export type DownloadFileReq = {
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 = {
name: string | undefined;
};
@@ -226,6 +302,7 @@ export type Scenario = {
averageRating: number | undefined;
reviewsCount: number | undefined;
difficulty: string | undefined;
+ music_playlist_id?: number;
};
export type Story = {
@@ -504,6 +581,17 @@ export type ScreenState = {
timerRemainingSeconds: number | undefined;
timerState: ScreenTimerState | 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 = {
@@ -531,6 +619,40 @@ export type GetGameScreensRsp = {
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 = {
screenID: string | undefined;
accessToken: string | undefined;
@@ -1013,6 +1135,13 @@ export interface EveningDetectiveServer {
GetPermissions(request: GetPermissionsReq): Promise;
UploadFile(request: UploadFileReq): Promise;
DownloadFile(request: DownloadFileReq): Promise;
+ GetPlaylists(request: GetPlaylistsReq): Promise;
+ CreatePlaylist(request: CreatePlaylistReq): Promise;
+ UpdatePlaylist(request: UpdatePlaylistReq): Promise;
+ DeletePlaylist(request: DeletePlaylistReq): Promise;
+ UploadPlaylistTrack(request: UploadPlaylistTrackReq): Promise;
+ DeletePlaylistTrack(request: DeletePlaylistTrackReq): Promise;
+ SetScenarioPlaylist(request: SetScenarioPlaylistReq): Promise;
AddScenario(request: AddScenarioReq): Promise;
GetMyScenarios(request: GetMyScenariosReq): Promise;
GetScenariosCatalog(request: GetScenariosCatalogReq): Promise;
@@ -1046,6 +1175,9 @@ export interface EveningDetectiveServer {
// Получить состояние экрана по его токену. Вызывается самим экраном.
GetScreenState(request: GetScreenStateReq): Promise;
GetGameScreens(request: GetGameScreensReq): Promise;
+ GetGameMusic(request: GetGameMusicReq): Promise;
+ SetGameMusic(request: SetGameMusicReq): Promise;
+ AdvanceScreenMusic(request: AdvanceScreenMusicReq): Promise;
// Привязать экран к игре. Требует авторизацию организатора, автора или администратора.
BindScreen(request: BindScreenReq): Promise;
// Отвязать экран от игры. Требует авторизацию организатора, автора или администратора.
@@ -1477,6 +1609,143 @@ export function createEveningDetectiveServerClient(
method: "DownloadFile",
}) as Promise;
},
+ 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;
+ },
+ 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;
+ },
+ 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;
+ },
+ 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;
+ },
+ 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;
+ },
+ 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;
+ },
+ 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;
+ },
AddScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/scenario`; // eslint-disable-line quotes
const body = JSON.stringify(request);
@@ -2045,6 +2314,63 @@ export function createEveningDetectiveServerClient(
method: "GetGameScreens",
}) as Promise;
},
+ 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;
+ },
+ 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;
+ },
+ 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;
+ },
BindScreen(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.screenID) {
throw new Error("missing required field request.screenID");
diff --git a/src/components/GamePage.vue b/src/components/GamePage.vue
index f8c9cef..f77f96b 100644
--- a/src/components/GamePage.vue
+++ b/src/components/GamePage.vue
@@ -27,6 +27,9 @@ import type {
Game,
GameReview,
MapItem,
+ MusicPlaybackState,
+ MusicState,
+ Playlist,
Question,
ResultsQuestion,
ScreenCommand,
@@ -114,6 +117,8 @@ const activeTeamCount = computed(
() => (game.value.teams || []).filter((team) => team.participationStatus !== 'cancelled').length,
)
const screens = ref([])
+const playlists = ref([])
+const music = ref()
const screenPlan = ref()
const screenPlanDraft = ref([])
const screenPlanDirty = ref(false)
@@ -185,6 +190,17 @@ async function 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() {
return screenPlanMode.value === 'edit'
}
@@ -360,6 +376,81 @@ async function setSoundScreen(screen: ScreenState) {
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) {
await client.UnbindScreen({ screenID: screen.ID })
await getGameScreens()
@@ -539,6 +630,7 @@ async function issueTickets() {
useSimplePolling(() => {
getGame(Number(gameId))
getGameScreens()
+ getGameMusic()
getScreenPlan()
getGameMap()
}, 2000)
@@ -811,9 +903,16 @@ useSimplePolling(() => {
diff --git a/src/components/GameScreensPanel.vue b/src/components/GameScreensPanel.vue
index f0f2322..0687665 100644
--- a/src/components/GameScreensPanel.vue
+++ b/src/components/GameScreensPanel.vue
@@ -1,23 +1,35 @@
@@ -58,6 +70,79 @@ const showBindingModal = ref(false)
:binding="props.screenBinding"
@scanned="(screenID, accessToken) => emit('bind', screenID, accessToken)"
/>
+
+ Музыка
+
+
+
+ Создать
+
+
+ Удалить плейлист
+
+
+ Добавить MP3
+
+
+
+ emit('setMusic', { playlistID: value || 0, trackIndex: 0, playback: 'MUSIC_PLAYBACK_STOPPED' })"
+ />
+
+ Удалить трек
+
+ emit('setMusic', { trackIndex: value })"
+ />
+ Старт
+ Пауза
+ Стоп
+ value != null && emit('setMusic', { volume: value })"
+ >
+ Громкость
+
+ emit('setMusic', { loop: value })"
+ >
+ Loop
+ Loop
+
+
+