@saooti/octopus-sdk 41.5.2 → 41.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 41.5.4 (12/02/2026)
4
+
5
+ **Misc**
6
+
7
+ - Ajouts d'options pour customiser les éléments de `TopBar`
8
+ - Ajout d'un slot `bottom` pour `PlaylistPage`
9
+
10
+ ## 41.5.3 (09/02/2026)
11
+
12
+ **Fix**
13
+
14
+ - Enregistrement stats d'écoutes pour vidéos Digiteka
15
+
16
+ **Misc**
17
+
18
+ - Correction double slash dans sharePath
19
+
3
20
  ## 41.5.2 (06/02/2026)
4
21
 
5
22
  **Misc**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saooti/octopus-sdk",
3
- "version": "41.5.2",
3
+ "version": "41.5.4",
4
4
  "private": false,
5
5
  "description": "Javascript SDK for using octopus",
6
6
  "author": "Saooti",
@@ -0,0 +1,17 @@
1
+ import { ModuleApi } from "./apiConnection";
2
+ import classicApi from "./classicApi";
3
+
4
+ /**
5
+ * Request the download ID from a video
6
+ * @param podcastId The id of the podcast to be watched
7
+ */
8
+ async function watchPodcast(podcastId: number): Promise<string> {
9
+ return classicApi.fetchData<string>({
10
+ api: ModuleApi.DEFAULT,
11
+ path: 'video/watch/' + podcastId
12
+ });
13
+ }
14
+
15
+ export const videoApi = {
16
+ watchPodcast
17
+ }
@@ -14,10 +14,10 @@ export const usePlayerLogicProgress = ()=>{
14
14
  const authStore = useAuthStore();
15
15
 
16
16
  const intervalToSend = computed(() => {
17
- if(lastSend.value<180){
17
+ if(lastSend.value < 180){
18
18
  return 10;
19
19
  }
20
- if(lastSend.value<1800){
20
+ if(lastSend.value < 1800){
21
21
  return 30;
22
22
  }
23
23
  return 60;
@@ -46,7 +46,9 @@ export const usePlayerLogicProgress = ()=>{
46
46
  return;
47
47
  }
48
48
  const audioPlayer: HTMLAudioElement | null = document.querySelector("#audio-player");
49
- if (!audioPlayer) return;
49
+ if (!audioPlayer) {
50
+ return;
51
+ }
50
52
  audioPlayer.currentTime = playerStore.playerSeekTime;
51
53
  });
52
54
 
@@ -72,12 +74,15 @@ export const usePlayerLogicProgress = ()=>{
72
74
  },
73
75
  });
74
76
  setDownloadId(downloadIdFetched);
75
- } catch {
77
+ } catch(e) {
76
78
  downloadId.value = null;
77
- console.log("ERROR downloadId");
79
+ console.error("ERROR downloadId", e);
78
80
  }
79
81
  }
80
82
 
83
+ /**
84
+ * @param currentTime: Temps en seconds
85
+ */
81
86
  function onTimeUpdateProgress(currentTime: number): void {
82
87
  if (!downloadId.value) {
83
88
  return;
@@ -92,7 +97,7 @@ export const usePlayerLogicProgress = ()=>{
92
97
  } else {
93
98
  const newListenTime = currentTime - notListenTime.value;
94
99
  const diffTime = newListenTime - listenTime.value;
95
- if(diffTime > 0 && diffTime<1){
100
+ if(diffTime > 0 && diffTime<1) {
96
101
  listenTime.value = newListenTime;
97
102
  }
98
103
  }
@@ -104,7 +109,9 @@ export const usePlayerLogicProgress = ()=>{
104
109
  }
105
110
 
106
111
  async function endListeningProgress(): Promise<void> {
107
- if (!downloadId.value) return;
112
+ if (!downloadId.value) {
113
+ return;
114
+ }
108
115
  await sendListeningProgress(listenTime.value);
109
116
  downloadId.value = null;
110
117
  notListenTime.value = 0;
@@ -120,24 +127,26 @@ export const usePlayerLogicProgress = ()=>{
120
127
  paramUrlLive="&url="+encodeURI(playerStore.playerHlsUrl);
121
128
  urlLiveSent.value = true;
122
129
  }
130
+
131
+ // Send listening updates if listening lasted more than 30 seconds
123
132
  try {
124
133
  await classicApi.putData<string | null>({
125
134
  api: 0,
126
135
  path:"podcast/listen/" +downloadId.value +"?seconds=" +Math.round(listenTime)+paramUrlLive,
127
136
  isNotAuth:true
128
137
  });
129
- } catch {
138
+ } catch(e) {
130
139
  //Do nothing
140
+ console.error(e);
131
141
  }
132
142
  }
133
143
 
134
-
135
- return {
144
+ return {
136
145
  listenTime,
137
146
  downloadId,
138
147
  initLiveDownloadId,
139
148
  setDownloadId,
140
149
  onTimeUpdateProgress,
141
150
  endListeningProgress
142
- }
143
- }
151
+ }
152
+ }
@@ -1,5 +1,5 @@
1
1
  import { OrganisationAttributes } from "@/stores/class/general/organisation";
2
- import { RouteLocation, RouteLocationAsRelativeTyped, RouteLocationNormalized, useRouter } from "vue-router";
2
+ import { RouteLocationAsRelativeTyped, useRouter } from "vue-router";
3
3
  import { useSeoTitleUrl } from "../route/useSeoTitleUrl";
4
4
 
5
5
  export const useSharePath = () => {
@@ -37,7 +37,11 @@ export const useSharePath = () => {
37
37
  */
38
38
  function getSharePath(route: RouteLocationAsRelativeTyped, attributes?: OrganisationAttributes): string {
39
39
  const resolved = router.resolve(route);
40
- return getBaseSharePath(attributes) + resolved.path;
40
+ // Remove trailing slash
41
+ const base = getBaseSharePath(attributes).replace(/\/$/, '');
42
+ // Remove leading slash
43
+ const path = resolved.path.replace(/^\//, '');
44
+ return base + '/' + path;
41
45
  }
42
46
 
43
47
  type GetSmartLinkParam = {
@@ -9,7 +9,7 @@
9
9
  <AppsIcon :size="30" />
10
10
  </button>
11
11
  <router-link
12
- v-if="isAuthenticatedWithOrga && authStore.isRoleContribution"
12
+ v-if="displayUpload"
13
13
  :title="t('Upload')"
14
14
  to="/main/priv/upload"
15
15
  class="btn admin-button hide-small-screen m-1 text-blue-octopus"
@@ -48,11 +48,15 @@ import { computed } from "vue";
48
48
  import { useI18n } from "vue-i18n";
49
49
  import { useRoute, useRouter } from "vue-router";
50
50
 
51
+ export interface HomeDropdownProps {
52
+ isEducation?: boolean;
53
+ mobileMenuDisplay?: boolean;
54
+ /** Display the upload button */
55
+ displayUpload?: boolean;
56
+ }
57
+
51
58
  //Props
52
- defineProps({
53
- isEducation: { default: false, type: Boolean },
54
- mobileMenuDisplay: { default: false, type: Boolean },
55
- })
59
+ defineProps<HomeDropdownProps>();
56
60
 
57
61
  //Composables
58
62
  const { t } = useI18n();
@@ -11,6 +11,7 @@
11
11
  :title-display="titleToDisplay"
12
12
  style="height: var(--header-size);"
13
13
  :class="headerBackgroundImage.length ? 'header-opacity':''"
14
+ :options="options?.topBarMainContent"
14
15
  />
15
16
  </header>
16
17
  <div v-if="generalStore.contentToDisplay" class="header-content-bg" :style="headerBackgroundImage" :class="{ scrolled: scrolled, 'header-force-blur':needToBlur }" >
@@ -30,7 +31,7 @@
30
31
 
31
32
  <script setup lang="ts">
32
33
  import {useImageProxy} from "../composable/useImageProxy";
33
- import TopBarMainContent from "./TopBarMainContent.vue";
34
+ import TopBarMainContent, { type TopBarMainContentOptions } from "./TopBarMainContent.vue";
34
35
  import { computed, defineAsyncComponent, onBeforeUnmount, onMounted, ref, watch } from "vue";
35
36
  import { useAuthStore } from "../../stores/AuthStore";
36
37
  import { useGeneralStore } from "../../stores/GeneralStore";
@@ -44,6 +45,11 @@ const SubscribeButtons = defineAsyncComponent(
44
45
  () => import("../display/sharing/SubscribeButtons.vue"),
45
46
  );
46
47
 
48
+ defineProps<{
49
+ options?: {
50
+ topBarMainContent?: TopBarMainContentOptions
51
+ }
52
+ }>();
47
53
 
48
54
  //Data
49
55
  const scrolled = ref(false);
@@ -138,6 +138,7 @@
138
138
  <HomeDropdown
139
139
  :is-education="generalStore.platformEducation"
140
140
  :mobile-menu-display="mobileMenuDisplay"
141
+ v-bind="options?.homeDropdown"
141
142
  />
142
143
  <router-link
143
144
  v-show="!isPhone && !inContentDisplayPage"
@@ -160,7 +161,7 @@ import ChevronDownIcon from "vue-material-design-icons/ChevronDown.vue";
160
161
  import MagnifyIcon from "vue-material-design-icons/Magnify.vue";
161
162
  import { useRubriquesFilterComputed } from "../composable/route/useRubriquesFilterComputed";
162
163
  import { state } from "../../stores/ParamSdkStore";
163
- import HomeDropdown from "./HomeDropdown.vue";
164
+ import HomeDropdown, { type HomeDropdownProps } from "./HomeDropdown.vue";
164
165
  import {useImageProxy} from "../composable/useImageProxy";
165
166
  import { useFilterStore } from "../../stores/FilterStore";
166
167
  import { useAuthStore } from "../../stores/AuthStore";
@@ -170,13 +171,21 @@ import { useGeneralStore } from "../../stores/GeneralStore";
170
171
  import { useI18n } from "vue-i18n";
171
172
  const MobileMenu = defineAsyncComponent(() => import("./MobileMenu.vue"));
172
173
 
174
+ export interface TopBarMainContentOptions {
175
+ homeDropdown?: HomeDropdownProps;
176
+ }
177
+
178
+ export interface TopBarMainContentProps {
179
+ isPhone?: boolean;
180
+ titleDisplay?: string;
181
+ scrolled?: boolean;
182
+ };
173
183
 
174
184
  //Props
175
- const props = defineProps({
176
- isPhone: { default: false, type: Boolean },
177
- titleDisplay: { default: "", type: String },
178
- scrolled: { default: false, type: Boolean },
179
- })
185
+ const props = defineProps<TopBarMainContentProps & {
186
+ /** Props for subcomponents */
187
+ options?: TopBarMainContentOptions;
188
+ }>();
180
189
 
181
190
 
182
191
  //Composables
@@ -6,7 +6,11 @@
6
6
  </button>
7
7
  <div class="video-wrapper">
8
8
  <PlayerYoutubeEmbed v-if="youtubeId" :youtube-id="youtubeId" />
9
- <PlayerVideoDigiteka v-else-if="!playerStore.playerLive" :video-id="playerStore.playerPodcast?.video?.videoId" />
9
+ <PlayerVideoDigiteka
10
+ v-else-if="!playerStore.playerLive"
11
+ :video-id="playerStore.playerPodcast?.video?.videoId"
12
+ :podcast-id="playerStore.playerPodcast.podcastId"
13
+ />
10
14
  <PlayerVideoHls v-else :hls-url="hlsVideoUrl" :is-secured="isSecured"/>
11
15
  </div>
12
16
  </template>
@@ -23,9 +23,13 @@ import { useI18n } from "vue-i18n";
23
23
  import SnackBar from "../../SnackBar.vue";
24
24
  import { computed, onMounted, useTemplateRef } from "vue";
25
25
 
26
+ import { usePlayerLogicProgress } from "../../../composable/player/usePlayerLogicProgress";
27
+ import { videoApi } from "../../../../api/videoApi";
28
+
26
29
  //Props
27
30
  const props = defineProps({
28
- videoId: { default: undefined, type: String },
31
+ videoId: { type: String, required: true },
32
+ podcastId: { type: Number, required: false, default: null },
29
33
  responsive: { default: false, type: Boolean },
30
34
  })
31
35
 
@@ -34,7 +38,7 @@ const snackBarRef = useTemplateRef('snackbar');
34
38
 
35
39
  //Composables
36
40
  const { t } = useI18n();
37
-
41
+ const { onTimeUpdateProgress, setDownloadId } = usePlayerLogicProgress();
38
42
 
39
43
  //Computed
40
44
  const srcVideo = computed(() => {
@@ -45,13 +49,30 @@ const srcVideo = computed(() => {
45
49
  );
46
50
  });
47
51
 
48
- onMounted(()=>{
52
+ onMounted(async()=>{
49
53
  if (undefined === props.videoId) {
50
54
  (snackBarRef?.value as InstanceType<typeof SnackBar>).open(t("Podcast play error"));
51
55
  }
52
- })
53
56
 
57
+ // #14118 cf https://support.digiteka.com/fr/API/Iframe#h-3-r%C3%A9ception-des-%C3%A9v%C3%A9nements-du-player
58
+ window.addEventListener('message', event => {
59
+ if (typeof event.data !== 'string') {
60
+ return;
61
+ }
62
+
63
+ const data = Object.fromEntries(event.data.split('&').map(d => d.split('=')));
64
+ if (data.event === 'timeupdate') {
65
+ onTimeUpdateProgress(parseFloat(data.time));
66
+ }
67
+ });
68
+
69
+ if (props.podcastId !== undefined) {
70
+ const downloadId = await videoApi.watchPodcast(props.podcastId);
71
+ setDownloadId(downloadId);
72
+ }
73
+ })
54
74
  </script>
55
- <style lang="scss">
75
+
76
+ <style scoped lang="scss">
56
77
  @use "../../../../style/videoPlayer";
57
78
  </style>
@@ -57,6 +57,8 @@
57
57
  />
58
58
  </section>
59
59
  </div>
60
+
61
+ <slot name="bottom" v-bind="{ playlist }" />
60
62
  </template>
61
63
  <ClassicLoading
62
64
  :loading-text="!loaded ? t('Loading content ...') : undefined"
@@ -33,7 +33,12 @@
33
33
  />
34
34
  </div>
35
35
  </template>
36
- <PlayerVideoDigiteka v-else :video-id="videoId" :responsive="true" />
36
+ <PlayerVideoDigiteka
37
+ v-else
38
+ :video-id="videoId"
39
+ :podcast-id="podcastId"
40
+ responsive
41
+ />
37
42
  </div>
38
43
  <div class="w-30-responsive info-video-container">
39
44
  <div class="d-flex flex-column flex-grow-1 w-100">
@@ -0,0 +1,43 @@
1
+ import '@tests/mocks/useRouter';
2
+ import '@tests/mocks/i18n';
3
+
4
+ import HomeDropdown from '@/components/misc/HomeDropdown.vue';
5
+ import { mount } from '@tests/utils';
6
+ import { describe, expect, it } from 'vitest';
7
+
8
+ describe('HomeDropdown - displayUpload prop', () => {
9
+ it('displays the upload button when displayUpload is true', async () => {
10
+ const wrapper = await mount(HomeDropdown, {
11
+ props: {
12
+ displayUpload: true
13
+ },
14
+ stubs: ['ClassicPopover', 'UserButtonContent', 'AppsIcon', 'AccountIcon', 'DownloadIcon']
15
+ });
16
+
17
+ const uploadButton = wrapper.find('a[title="Upload"]');
18
+ expect(uploadButton.exists()).toBe(true);
19
+ expect(uploadButton.attributes('to')).toBe('/main/priv/upload');
20
+ });
21
+
22
+ it('does not display the upload button when displayUpload is false', async () => {
23
+ const wrapper = await mount(HomeDropdown, {
24
+ props: {
25
+ displayUpload: false
26
+ },
27
+ stubs: ['ClassicPopover', 'UserButtonContent', 'AppsIcon', 'AccountIcon', 'DownloadIcon']
28
+ });
29
+
30
+ const uploadButton = wrapper.find('a[title="Upload"]');
31
+ expect(uploadButton.exists()).toBe(false);
32
+ });
33
+
34
+ it('does not display the upload button when displayUpload is undefined', async () => {
35
+ const wrapper = await mount(HomeDropdown, {
36
+ props: {},
37
+ stubs: ['ClassicPopover', 'UserButtonContent', 'AppsIcon', 'AccountIcon', 'DownloadIcon']
38
+ });
39
+
40
+ const uploadButton = wrapper.find('a[title="Upload"]');
41
+ expect(uploadButton.exists()).toBe(false);
42
+ });
43
+ });