@@ -316,6 +316,14 @@
class="rounded text-primary bg-gray-700 border-none w-4 h-4 pointer-events-none"
checked>
@@ -572,14 +580,42 @@
originalVideos: []
};
- this.config = { server: '', user: '', token: '', userId: '', useStatic: true, autoplay: true, deleteMode: false };
+ this.config = { server: '', user: '', token: '', userId: '', useStatic: true, autoplay: true, shortDrama: false, deleteMode: false };
this.csTimer = null;
this.dom = {};
+ this.config.deviceName = this.getDeviceName();
this.init();
}
+
+ getDeviceName() {
+ const ua = navigator.userAgent;
+ if (/android/i.test(ua)) {
+ const match = ua.match(/\(([^)]+)\)/);
+ if (match && match[1]) {
+ const parts = match[1].split(';');
+ for (let p of parts) {
+ p = p.trim();
+ if (/^linux/i.test(p) || /^u$/i.test(p) || /^android/i.test(p) || /^[a-z]{2}-[a-z]{2}$/i.test(p) || /wv/.test(p)) continue;
+ const model = p.split('Build/')[0].trim();
+ if (model) return `Android (${model})`;
+ }
+ }
+ return "Android";
+ } else if (/ipad|macintosh/i.test(ua) && navigator.maxTouchPoints > 1) {
+ return "iPad";
+ } else if (/iphone/i.test(ua)) {
+ return "iPhone";
+ } else if (/macintosh/i.test(ua)) {
+ return "macOS";
+ } else if (/windows/i.test(ua)) {
+ return "Windows";
+ }
+ return "Web Browser";
+ }
+
isAnyModalOpen() {
const modals = ['mediaInfoModal', 'deleteConfirmModal', 'profilePage', 'libraryModal'];
return modals.some(id => {
@@ -625,12 +661,21 @@
lucide.createIcons();
if (this.config.server && this.config.token) {
- this.fetchVideos(this.state.currentLibraryId);
+ this.fetchVideos(this.state.currentLibraryId, null, false, this.state.playMode === 'random');
} else {
setTimeout(() => this.toggleModal('profilePage', true), 500);
}
+
+ // ─── Branding Egg ───
+ console.log(
+ "%c EmbyX %c Designed by Juneix %c",
+ "background:#3b82f6;color:#fff;padding:2px 6px;border-radius:4px 0 0 4px;font-weight:bold;",
+ "background:#1e293b;color:#fff;padding:2px 6px;border-radius:0 4px 4px 0;",
+ "background:transparent"
+ );
}
+
cacheDOM() {
const ids = [
'videoContainer', 'clickToPlayOverlay', 'loading', 'toast',
@@ -639,7 +684,7 @@
'favoriteBtn', 'scaleBtn', 'muteBtn', 'fullscreenBtn', 'deleteBtn',
'mediaInfoModal', 'mediaInfoContent', 'closeMediaInfoBtn',
'deleteConfirmModal', 'confirmDeleteBtn', 'cancelDeleteBtn', 'deleteFileName',
- 'configServer', 'configUser', 'configPwd', 'configStatic', 'configAutoplay', 'configDeleteMode'
+ 'configServer', 'configUser', 'configPwd', 'configStatic', 'configAutoplay', 'configShortDrama', 'configDeleteMode'
];
ids.forEach(id => this.dom[id] = document.getElementById(id));
this.dom.rightToolbar = document.querySelector('.right-toolbar');
@@ -652,6 +697,7 @@
this.config.userId = localStorage.getItem('emby_uid') || '';
this.config.useStatic = localStorage.getItem('emby_static') !== 'false';
this.config.autoplay = localStorage.getItem('emby_autoplay') !== 'false';
+ this.config.shortDrama = localStorage.getItem('emby_shortdrama') === 'true';
this.config.deleteMode = localStorage.getItem('emby_delete_mode') === 'true';
// Unique Device ID for session management
@@ -669,6 +715,7 @@
const savedPlayMode = localStorage.getItem('emby_play_mode');
if (savedPlayMode !== null) this.state.playMode = savedPlayMode;
+ if (this.config.shortDrama) this.state.playMode = 'sequence';
const savedLibId = localStorage.getItem('emby_lib_id');
const savedLibType = localStorage.getItem('emby_lib_type');
@@ -685,6 +732,7 @@
}
if (this.dom.configStatic) this.dom.configStatic.checked = this.config.useStatic;
if (this.dom.configAutoplay) this.dom.configAutoplay.checked = this.config.autoplay;
+ if (this.dom.configShortDrama) this.dom.configShortDrama.checked = this.config.shortDrama;
if (this.dom.configDeleteMode) this.dom.configDeleteMode.checked = this.config.deleteMode;
// Sync UI based on config
@@ -696,34 +744,43 @@
}
async saveConfig() {
- const server = this.dom.configServer.value.trim().replace(/\/$/, "");
+ let server = this.dom.configServer.value.trim().replace(/\/$/, "");
const user = this.dom.configUser.value.trim();
const pwd = this.dom.configPwd.value.trim();
+
+ // Auto-fill protocol and port
+ if (server && !server.startsWith('http')) {
+ const host = server.split(':')[0];
+ const isIP = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(host) || host === 'localhost' || host === '127.0.0.1';
+
+ // If IP and no port specified, append default 8096
+ if (isIP && !server.includes(':')) {
+ server += ':8096';
+ }
+
+ server = (isIP ? 'http://' : 'https://') + server;
+ }
+
const isStatic = this.dom.configStatic.checked;
const autoplay = this.dom.configAutoplay.checked;
+ const shortDrama = this.dom.configShortDrama.checked;
const deleteMode = this.dom.configDeleteMode.checked;
if (!server || !user) return this.showToast('Please fill in all fields');
+
this.toggleLoading(true);
try {
if (/^\*+$/.test(pwd) && this.config.token && this.config.server === server && this.config.user === user) {
localStorage.setItem('emby_static', isStatic);
localStorage.setItem('emby_autoplay', autoplay);
+ localStorage.setItem('emby_shortdrama', shortDrama);
localStorage.setItem('emby_delete_mode', deleteMode);
- this.config.useStatic = isStatic;
- this.config.autoplay = autoplay;
- this.config.deleteMode = deleteMode;
- this.state.isAutoplay = autoplay;
- this.dom.deleteBtn.style.display = deleteMode ? 'flex' : 'none';
- if (this.dom.playModeBtn) {
- this.dom.playModeBtn.innerHTML = `
`;
- lucide.createIcons();
- }
-
- this.showToast('✅ Settings saved');
- this.toggleModal('profilePage', false);
+ // Clear data cache, keep core settings and reload
+ localStorage.removeItem('emby_views_cache');
+ localStorage.removeItem('emby_favorites');
+ location.reload();
return;
}
@@ -731,7 +788,7 @@
method: 'POST',
headers: {
'Content-Type': 'application/json',
- 'X-Emby-Authorization': `Emby Client="EmbyX", Device="Web", DeviceId="${this.config.deviceId}", Version="${this.config.appVersion}"`
+ 'X-Emby-Authorization': `Emby Client="EmbyX", Device="${this.config.deviceName}", DeviceId="${this.config.deviceId}", Version="${this.config.appVersion}"`
},
body: JSON.stringify({ Username: user, Pw: pwd })
});
@@ -755,9 +812,10 @@
localStorage.setItem('emby_uid', data.SessionInfo.UserId);
localStorage.setItem('emby_static', isStatic);
localStorage.setItem('emby_autoplay', autoplay);
+ localStorage.setItem('emby_shortdrama', shortDrama);
localStorage.setItem('emby_delete_mode', deleteMode);
- this.config = { ...this.config, server, user, token: data.AccessToken, userId: data.SessionInfo.UserId, useStatic: isStatic, autoplay, deleteMode };
+ this.config = { ...this.config, server, user, token: data.AccessToken, userId: data.SessionInfo.UserId, useStatic: isStatic, autoplay, shortDrama, deleteMode };
const pwdLen = Math.max(1, pwd.length);
localStorage.setItem('emby_pwd_len', pwdLen);
@@ -822,25 +880,30 @@
}
}
- async fetchVideos(parentId = null, ids = null, isLoadMore = false, isRandom = false) {
- this.toggleLoading(true);
+ async fetchVideos(parentId = null, ids = null, isLoadMore = false, isRandom = false, isAppend = false) {
+ this.toggleLoading(!isAppend);
try {
const { server, token, userId } = this.config;
if (!server || !token) {
- this.showToast('Please config server first');
+ if (!isAppend) this.showToast('Please config server first');
return;
}
- if (!isLoadMore) {
+ if (!isLoadMore && !isAppend) {
this.state.startIndex = 0;
}
const libraryId = parentId || this.state.currentLibraryId;
- const PAGE_SIZE = 99;
+ const PAGE_SIZE = 150;
// const MAX_GRID_VIDEOS = 1000;
- let url = `${server}/emby/Users/${userId}/Items?api_key=${token}&Recursive=true&IncludeItemTypes=Movie,Episode,Video&Limit=${PAGE_SIZE}&StartIndex=${this.state.startIndex || 0}&Fields=Overview,Path,RunTimeTicks,MediaSources`;
+ let fetchStartIndex = this.state.startIndex || 0;
+ if (isAppend) {
+ fetchStartIndex += this.state.videos.length;
+ }
+
+ let url = `${server}/emby/Users/${userId}/Items?api_key=${token}&Recursive=true&IncludeItemTypes=Movie,Episode,Video,MusicVideo&Limit=${PAGE_SIZE}&StartIndex=${fetchStartIndex}&Fields=Overview,Path,RunTimeTicks,MediaSources`;
if (ids) {
url += `&Ids=${ids}`;
@@ -851,14 +914,16 @@
} else if (libraryId === 'favorites') {
url += `&SortBy=DateCreated&SortOrder=Descending&Filters=IsFavorite`;
} else if (libraryId) {
- url += `&SortBy=DateCreated&SortOrder=Descending&ParentId=${libraryId}`;
+ const sortStr = this.config.shortDrama ? 'SortBy=Path&SortOrder=Ascending' : 'SortBy=DateCreated&SortOrder=Descending';
+ url += `&${sortStr}&ParentId=${libraryId}`;
} else {
- url += `&SortBy=DateCreated&SortOrder=Descending`;
+ const sortStr = this.config.shortDrama ? 'SortBy=Path&SortOrder=Ascending' : 'SortBy=DateCreated&SortOrder=Descending';
+ url += `&${sortStr}`;
}
const res = await fetch(url, {
headers: {
- 'X-Emby-Authorization': `Emby Client="EmbyX", Device="Web", DeviceId="${this.config.deviceId}", Version="${this.config.appVersion}"`
+ 'X-Emby-Authorization': `Emby Client="EmbyX", Device="${this.config.deviceName}", DeviceId="${this.config.deviceId}", Version="${this.config.appVersion}"`
}
});
if (!res.ok) {
@@ -867,23 +932,31 @@
let data = await res.json();
if (data && data.Items && data.Items.length > 0) {
- this.state.videos = data.Items;
- this.state.currentIndex = 0;
- this.state.totalCount = data.TotalRecordCount || 0;
-
- if (this.state.playMode === 'random') {
- this.state.originalVideos = [...this.state.videos];
- this._shuffleAroundCurrent();
- }
-
- if (this.state.viewMode === 'grid') {
- this.renderGridView();
+ if (isAppend) {
+ this.state.videos.push(...data.Items);
+ if (this.state.playMode === 'random' && this.state.originalVideos) {
+ this.state.originalVideos.push(...data.Items);
+ }
+ return;
} else {
- this.renderSlides();
- this.loadVideo(0);
+ this.state.videos = data.Items;
+ this.state.currentIndex = 0;
+ this.state.totalCount = data.TotalRecordCount || 0;
+
+ if (this.state.playMode === 'random') {
+ this.state.originalVideos = [...this.state.videos];
+ this._shuffleAroundCurrent();
+ }
+
+ if (this.state.viewMode === 'grid') {
+ this.renderGridView();
+ } else {
+ this.renderSlides();
+ this.loadVideo(0);
+ }
}
- } else if (isLoadMore) {
- this.showToast('No more videos');
+ } else if (isLoadMore || isAppend) {
+ if (!isAppend) this.showToast('No more videos');
this.state.startIndex = Math.max(0, (this.state.startIndex || 0));
this.toggleLoading(false);
return;
@@ -902,9 +975,13 @@
renderSlides() {
this.dom.videoContainer.innerHTML = '';
this.dom.videoContainer.className = 'relative w-full h-full transition-transform duration-300 ease-out';
- // Clear inline transition left by grid mode, so class-based animation works
- this.dom.videoContainer.style.transition = '';
+ // Temporarily disable transition for instant snap
+ this.dom.videoContainer.style.transition = 'none';
this.dom.videoContainer.style.transform = `translateY(-${this.state.currentIndex * 100}%)`;
+ // Force browser reflow to apply the instant transform
+ void this.dom.videoContainer.offsetHeight;
+ // Restore transition for subsequent manual swipes
+ this.dom.videoContainer.style.transition = '';
for (let i = 0; i < 3; i++) {
const el = document.createElement('div');
@@ -990,7 +1067,7 @@
}
}
- const PAGE_SIZE = 99;
+ const PAGE_SIZE = 150;
const totalCount = this.state.totalCount || 0;
const startIndex = this.state.startIndex || 0;
const currentPage = Math.floor(startIndex / PAGE_SIZE) + 1;
@@ -1005,7 +1082,7 @@
safeHeader.innerHTML = `
${libraryName}
-
+
${countStr}