-
-
-

-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentAirportCode }}
+
+ {{ currentMusicName }}
+
+
+
+
+
+
+
+
+
@@ -63,9 +79,13 @@
import { useI18n } from 'vue-i18n';
import { mapState, mapMutations } from 'vuex';
import { safePause, safePlay, safeLoad } from '../utilites';
-import Airports from './Airports.vue';
-import Music from './Music.vue';
-import Logo from '../assets/logo.svg';
+import { useAudioAnalyser } from '../composables/useAudioAnalyser';
+import Sphere from './Sphere.vue';
+import GlassPanel from './GlassPanel.vue';
+import DataList from './DataList.vue';
+import Starfield from './Starfield.vue';
+import ParticleCanvas from './ParticleCanvas.vue';
+import MobileDrawer from './MobileDrawer.vue';
export default {
name: 'Main',
@@ -75,6 +95,17 @@ export default {
t, safePause, safePlay, safeLoad,
};
},
+ data() {
+ return {
+ isMobile: false,
+ musicAudio: null,
+ airportAudio: null,
+ sphereRect: null,
+ musicAmplitude: 0,
+ airportAmplitude: 0,
+ amplitudeRafId: null,
+ };
+ },
computed: {
...mapState({
AirportsError: (state) => state.airports.error,
@@ -82,20 +113,29 @@ export default {
currentAirportUrl: (state) => state.airports.currentUrl,
currentMusicUrl: (state) => state.music.currentUrl,
MusicError: (state) => state.music.error,
+ airports: (state) => state.airports.list,
+ currentAirportCode: (state) => state.airports.currentCode,
+ airportVolume: (state) => state.airports.volume,
+ musicList: (state) => state.music.list,
+ currentMusicId: (state) => state.music.currentId,
+ musicVolume: (state) => state.music.volume,
}),
- isChristmasTime() {
- const currentDate = Date.now();
- const currentYear = new Date().getFullYear();
- const currentMonth = new Date().getMonth();
- const christmasStart = new Date(`${currentMonth === 11 ? currentYear : currentYear + 1}-12-20`).getTime();
- const christmasEnd = Date(christmasStart + 60 * 60 * 24 * 20 * 1000);
- return (currentDate > christmasStart) && (currentDate < christmasEnd);
+ currentMusicName() {
+ const station = this.musicList[this.currentMusicId];
+ return station ? station.name : '';
+ },
+ isBuffering() {
+ return this.$store.state.airports.status === 'pending' || this.$store.state.music.status === 'pending';
+ },
+ hasError() {
+ return this.$store.state.airports.status === 'failed' || this.$store.state.music.status === 'failed';
},
},
watch: {
appStatus(newVal) {
if (newVal === 'playing') {
this.play();
+ this.connectAnalysers();
}
if (newVal === 'paused') {
this.pause();
@@ -125,6 +165,54 @@ export default {
this.setAirportError(null);
}
},
+ airportVolume(newVal) {
+ if (this.$refs.airportPlayer) this.$refs.airportPlayer.volume = newVal;
+ },
+ musicVolume(newVal) {
+ if (this.$refs.musicPlayer) this.$refs.musicPlayer.volume = newVal;
+ },
+ },
+ mounted() {
+ this.musicAudio = useAudioAnalyser();
+ this.airportAudio = useAudioAnalyser();
+
+ if (this.appStatus === 'playing') {
+ this.connectAnalysers();
+ }
+
+ this.updateSphereRect();
+ this._resizeHandler = () => this.updateSphereRect();
+ window.addEventListener('resize', this._resizeHandler);
+
+ this._mobileQuery = window.matchMedia('(max-width: 767px)');
+ this.isMobile = this._mobileQuery.matches;
+ this._mobileHandler = (e) => { this.isMobile = e.matches; };
+ this._mobileQuery.addEventListener('change', this._mobileHandler);
+
+ this.startAmplitudeLoop();
+
+ this._keydownHandler = this.handleKeydown.bind(this);
+ window.addEventListener('keydown', this._keydownHandler);
+ },
+ beforeUnmount() {
+ if (this._keydownHandler) {
+ window.removeEventListener('keydown', this._keydownHandler);
+ }
+ if (this.musicAudio) this.musicAudio.disconnect();
+ if (this.airportAudio) this.airportAudio.disconnect();
+
+ if (this._resizeHandler) {
+ window.removeEventListener('resize', this._resizeHandler);
+ }
+
+ if (this._mobileQuery && this._mobileHandler) {
+ this._mobileQuery.removeEventListener('change', this._mobileHandler);
+ }
+
+ if (this.amplitudeRafId != null) {
+ cancelAnimationFrame(this.amplitudeRafId);
+ this.amplitudeRafId = null;
+ }
},
methods: {
...mapMutations([
@@ -134,7 +222,38 @@ export default {
'setMusicError',
'setMusicStatus',
'showToast',
+ 'setCurrentAirportCode',
+ 'setAirportVolume',
+ 'setCurrentMusicId',
+ 'setMusicVolume',
]),
+ handleKeydown(e) {
+ if (e.target.tagName === 'INPUT') {
+ if (e.key === 'Escape') {
+ e.target.blur();
+ e.target.value = '';
+ }
+ return;
+ }
+ switch (e.key) {
+ case ' ':
+ e.preventDefault();
+ this.toggleAppStatus();
+ break;
+ case '[':
+ this.setMusicVolume(Math.max(0, this.musicVolume - 0.1));
+ break;
+ case ']':
+ this.setMusicVolume(Math.min(1, this.musicVolume + 0.1));
+ break;
+ case '{':
+ this.setAirportVolume(Math.max(0, this.airportVolume - 0.1));
+ break;
+ case '}':
+ this.setAirportVolume(Math.min(1, this.airportVolume + 0.1));
+ break;
+ }
+ },
toggleAppStatus() {
const newStatus = this.appStatus === 'playing' ? 'paused' : 'playing';
this.setAppStatus(newStatus);
@@ -175,160 +294,110 @@ export default {
this.$refs.airportPlayer.play();
}
},
+ connectAnalysers() {
+ if (this.musicAudio && this.$refs.musicPlayer) {
+ this.musicAudio.connect(this.$refs.musicPlayer);
+ }
+ if (this.airportAudio && this.$refs.airportPlayer) {
+ this.airportAudio.connect(this.$refs.airportPlayer);
+ }
+ },
+ updateSphereRect() {
+ this.$nextTick(() => {
+ const sphereEl = this.$el?.querySelector('.sphere');
+ const mainEl = this.$el?.querySelector('.main-area') || this.$el;
+ if (!sphereEl || !mainEl) return;
+
+ const sphereBounds = sphereEl.getBoundingClientRect();
+ const mainBounds = mainEl.getBoundingClientRect();
+
+ this.sphereRect = {
+ x: sphereBounds.left - mainBounds.left,
+ y: sphereBounds.top - mainBounds.top,
+ width: sphereBounds.width,
+ height: sphereBounds.height,
+ };
+ });
+ },
+ startAmplitudeLoop() {
+ const loop = () => {
+ if (!document.hidden) {
+ this.musicAmplitude = this.musicAudio ? this.musicAudio.getAmplitude() : 0;
+ this.airportAmplitude = this.airportAudio ? this.airportAudio.getAmplitude() : 0;
+ }
+ this.amplitudeRafId = requestAnimationFrame(loop);
+ };
+ this.amplitudeRafId = requestAnimationFrame(loop);
+ },
},
- components: {
- Music,
- Airports,
- Logo,
- },
+ components: { Sphere, GlassPanel, DataList, Starfield, ParticleCanvas, MobileDrawer },
};
diff --git a/src/components/MobileDrawer.vue b/src/components/MobileDrawer.vue
new file mode 100644
index 0000000..2668d24
--- /dev/null
+++ b/src/components/MobileDrawer.vue
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Music.vue b/src/components/Music.vue
deleted file mode 100644
index 88aff6d..0000000
--- a/src/components/Music.vue
+++ /dev/null
@@ -1,94 +0,0 @@
-
-
-
-
-
diff --git a/src/components/ParticleCanvas.vue b/src/components/ParticleCanvas.vue
new file mode 100644
index 0000000..3a84e97
--- /dev/null
+++ b/src/components/ParticleCanvas.vue
@@ -0,0 +1,380 @@
+
+
+
+
+
+
+
diff --git a/src/components/Sphere.vue b/src/components/Sphere.vue
new file mode 100644
index 0000000..5ceaa94
--- /dev/null
+++ b/src/components/Sphere.vue
@@ -0,0 +1,209 @@
+
+
+
+
+
+
+
diff --git a/src/components/Starfield.vue b/src/components/Starfield.vue
new file mode 100644
index 0000000..9ccc073
--- /dev/null
+++ b/src/components/Starfield.vue
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
diff --git a/src/composables/useAudioAnalyser.js b/src/composables/useAudioAnalyser.js
new file mode 100644
index 0000000..e40c357
--- /dev/null
+++ b/src/composables/useAudioAnalyser.js
@@ -0,0 +1,146 @@
+/**
+ * useAudioAnalyser — Web Audio API frequency analyser for audio-reactive visuals.
+ *
+ * Wraps the Web Audio API to provide real-time frequency data from