@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 +17 -0
- package/package.json +1 -1
- package/src/api/videoApi.ts +17 -0
- package/src/components/composable/player/usePlayerLogicProgress.ts +21 -12
- package/src/components/composable/share/useSharePath.ts +6 -2
- package/src/components/misc/HomeDropdown.vue +9 -5
- package/src/components/misc/TopBar.vue +7 -1
- package/src/components/misc/TopBarMainContent.vue +15 -6
- package/src/components/misc/player/video/PlayerVideo.vue +5 -1
- package/src/components/misc/player/video/PlayerVideoDigiteka.vue +26 -5
- package/src/components/pages/PlaylistPage.vue +2 -0
- package/src/components/pages/VideoPage.vue +6 -1
- package/tests/components/misc/HomeDropdown.spec.ts +43 -0
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
|
@@ -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)
|
|
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.
|
|
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)
|
|
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 {
|
|
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
|
-
|
|
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="
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
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: {
|
|
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
|
-
|
|
75
|
+
|
|
76
|
+
<style scoped lang="scss">
|
|
56
77
|
@use "../../../../style/videoPlayer";
|
|
57
78
|
</style>
|
|
@@ -33,7 +33,12 @@
|
|
|
33
33
|
/>
|
|
34
34
|
</div>
|
|
35
35
|
</template>
|
|
36
|
-
<PlayerVideoDigiteka
|
|
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
|
+
});
|