Release 1.2.5

This commit is contained in:
imeed166 2026-05-10 14:02:12 +01:00
parent 4e8385ad07
commit f3ecda4ac7
6 changed files with 262 additions and 51 deletions

9
RELEASE_NOTES_1.2.5.md Normal file
View file

@ -0,0 +1,9 @@
# Cross Player 1.2.5
This patch release improves cross-device syncing for playback progress and fixes a pane-close regression that could reset the saved position back to the beginning.
Highlights:
- Improved synced `data.json` reload detection so playback position updates propagate more reliably to other devices.
- Added refresh checks on focus, app visibility return, and a periodic sync poll to keep queue progress current across devices.
- Updated the active player to reconcile synced queue positions when this device is not actively playing the file.
- Fixed closing the player pane from the middle of a file so the saved position no longer resets to the beginning.

143
main.js
View file

@ -2316,6 +2316,9 @@ var CrossPlayerPlugin = class extends import_obsidian.Plugin {
// bytes
this.limitingDevice = "";
this.lastSyncIssueKey = "";
this.lastKnownDataMtime = 0;
this.lastKnownDataSize = 0;
this.isReloadingSyncedData = false;
}
getLastGoodWatchedFolder() {
try {
@ -2356,6 +2359,48 @@ var CrossPlayerPlugin = class extends import_obsidian.Plugin {
this.lastSyncIssueKey = "";
}
}
getPluginDataPath() {
return `${this.manifest.dir}/data.json`;
}
async getPluginDataStat() {
try {
return await this.app.vault.adapter.stat(this.getPluginDataPath());
} catch (error) {
console.warn("[Cross Player] Failed to stat data.json", error);
return null;
}
}
async refreshTrackedDataFileState() {
var _a, _b;
const stat = await this.getPluginDataStat();
this.lastKnownDataMtime = (_a = stat == null ? void 0 : stat.mtime) != null ? _a : 0;
this.lastKnownDataSize = (_b = stat == null ? void 0 : stat.size) != null ? _b : 0;
}
async reloadSyncedDataIfChanged(force = false) {
var _a, _b;
if (this.isReloadingSyncedData)
return;
const stat = await this.getPluginDataStat();
const nextMtime = (_a = stat == null ? void 0 : stat.mtime) != null ? _a : 0;
const nextSize = (_b = stat == null ? void 0 : stat.size) != null ? _b : 0;
const changed = force || nextMtime !== this.lastKnownDataMtime || nextSize !== this.lastKnownDataSize;
if (!changed)
return;
this.isReloadingSyncedData = true;
try {
await this.loadData();
await this.validatePluginSyncHealth();
await this.refreshTrackedDataFileState();
if (this.mainView) {
this.mainView.handleSyncedDataReload();
}
if (this.listView) {
this.listView.refresh();
}
} finally {
this.isReloadingSyncedData = false;
}
}
async validatePluginSyncHealth() {
var _a, _b, _c, _d;
try {
@ -2421,6 +2466,7 @@ var CrossPlayerPlugin = class extends import_obsidian.Plugin {
}
async onload() {
await this.loadData();
await this.refreshTrackedDataFileState();
await this.validatePluginSyncHealth();
this.calculateDynamicLimit();
this.debouncedUpdateDeviceStatus = (0, import_obsidian.debounce)(async () => {
@ -2589,9 +2635,7 @@ var CrossPlayerPlugin = class extends import_obsidian.Plugin {
id: "reload-data",
name: "Reload Data from Disk",
callback: async () => {
await this.loadData();
if (this.listView)
this.listView.refresh();
await this.reloadSyncedDataIfChanged(true);
new import_obsidian.Notice("Data reloaded.");
}
});
@ -2628,13 +2672,10 @@ var CrossPlayerPlugin = class extends import_obsidian.Plugin {
}
}
});
this.debouncedReload = (0, import_obsidian.debounce)(async () => {
await this.reloadSyncedDataIfChanged();
}, 1e3, true);
if (import_obsidian.Platform.isDesktop) {
this.debouncedReload = (0, import_obsidian.debounce)(async () => {
await this.loadData();
await this.validatePluginSyncHealth();
if (this.listView)
this.listView.refresh();
}, 1e3, true);
try {
const path = require("path");
const fs = require("fs");
@ -2656,6 +2697,14 @@ var CrossPlayerPlugin = class extends import_obsidian.Plugin {
this.registerDomEvent(window, "focus", () => {
this.debouncedReload();
});
this.registerDomEvent(document, "visibilitychange", () => {
if (document.visibilityState === "visible") {
this.debouncedReload();
}
});
this.registerInterval(window.setInterval(() => {
this.debouncedReload();
}, 5e3));
this.registerWatchers();
if (this.data.settings.watchedFolder) {
this.scanFolder(this.data.settings.watchedFolder);
@ -2757,6 +2806,7 @@ var CrossPlayerPlugin = class extends import_obsidian.Plugin {
if (settings.watchedFolder) {
this.setLastGoodWatchedFolder(settings.watchedFolder);
}
await this.refreshTrackedDataFileState();
}
getTodayStatKey() {
return new Date().toISOString().slice(0, 10);
@ -2821,6 +2871,7 @@ var CrossPlayerPlugin = class extends import_obsidian.Plugin {
async saveData(refresh = true) {
this.rememberQueueScrollPosition();
await super.saveData(this.data);
await this.refreshTrackedDataFileState();
if (refresh && this.listView)
this.listView.refresh();
}
@ -3907,8 +3958,7 @@ var CrossPlayerListView = class extends import_obsidian.ItemView {
(0, import_obsidian.setIcon)(refreshBtn, "refresh-cw");
refreshBtn.ariaLabel = "Refresh Data";
refreshBtn.onclick = async () => {
await this.plugin.loadData();
this.refresh();
await this.plugin.reloadSyncedDataIfChanged(true);
new import_obsidian.Notice("Data reloaded.");
};
const cleanBtn = titleRow.createDiv({ cls: "clickable-icon" });
@ -4326,6 +4376,64 @@ var CrossPlayerMainView = class extends import_obsidian.ItemView {
await this.plugin.updateStatus(this.currentItem.id, "completed");
}
}
async persistPlaybackSnapshotOnClose() {
if (!this.currentItem)
return;
const queueItem = this.plugin.data.queue.find((item) => {
var _a;
return item.id === ((_a = this.currentItem) == null ? void 0 : _a.id);
}) || this.plugin.data.queue.find((item) => {
var _a;
return item.path === ((_a = this.currentItem) == null ? void 0 : _a.path);
});
if (!queueItem)
return;
const currentTime = this.videoEl && isFinite(this.videoEl.currentTime) ? this.videoEl.currentTime : queueItem.position || 0;
const duration = this.videoEl && isFinite(this.videoEl.duration) && this.videoEl.duration > 0 ? this.videoEl.duration : queueItem.duration || 0;
queueItem.position = currentTime;
this.currentItem.position = currentTime;
if (duration > 0 && Math.abs((queueItem.duration || 0) - duration) > 1) {
queueItem.duration = duration;
}
this.currentItem.duration = queueItem.duration;
const progress = duration > 0 ? currentTime / duration : 0;
if (progress > 0.95) {
queueItem.status = "completed";
queueItem.finished = true;
this.plugin.recordConsumption(queueItem);
this.currentItem.status = "completed";
this.currentItem.finished = true;
} else if (queueItem.status === "playing") {
queueItem.status = queueItem.finished ? "completed" : "pending";
this.currentItem.status = queueItem.status;
}
await this.plugin.saveData();
}
handleSyncedDataReload() {
if (!this.currentItem)
return;
const syncedItem = this.plugin.data.queue.find((item) => {
var _a;
return item.id === ((_a = this.currentItem) == null ? void 0 : _a.id);
}) || this.plugin.data.queue.find((item) => {
var _a;
return item.path === ((_a = this.currentItem) == null ? void 0 : _a.path);
});
if (!syncedItem)
return;
const isActivelyPlayingLocally = !!this.videoEl && this.isCurrentPlaybackSource() && !this.videoEl.paused && !this.videoEl.ended;
this.currentItem = syncedItem;
if (this.videoEl && !isActivelyPlayingLocally && isFinite(this.videoEl.duration) && this.videoEl.duration > 0) {
const targetPosition = Math.max(0, Math.min(this.videoEl.duration, syncedItem.position || 0));
if (Math.abs(this.videoEl.currentTime - targetPosition) > 1) {
this.videoEl.currentTime = targetPosition;
}
}
this.updateOverlayProgress();
if (this.plugin.listView) {
this.plugin.listView.updateStats();
}
}
async onOpen() {
const container = this.contentEl;
container.empty();
@ -4451,23 +4559,12 @@ var CrossPlayerMainView = class extends import_obsidian.ItemView {
};
}
async onClose() {
const closingItem = this.currentItem;
const shouldResetStatus = (closingItem == null ? void 0 : closingItem.status) === "playing";
if (this.videoEl) {
this.videoEl.pause();
await this.syncCurrentItemDuration();
await this.persistCurrentPlaybackPosition(true);
await this.syncCompletionStatusFromPlayback();
await this.persistPlaybackSnapshotOnClose();
this.videoEl.removeAttribute("src");
this.videoEl.load();
}
if (closingItem && shouldResetStatus) {
if (closingItem.finished) {
await this.plugin.updateStatus(closingItem.id, "completed");
} else {
await this.plugin.updateStatus(closingItem.id, "pending");
}
}
this.activeMediaSrc = null;
this.currentItem = null;
this.lastPositionPersist = 0;

View file

@ -1,7 +1,7 @@
{
"id": "cross-player",
"name": "Cross Player",
"version": "1.2.4",
"version": "1.2.5",
"minAppVersion": "0.15.0",
"description": "A media player plugin that watches a folder and plays media.",
"author": "Imed Ghomari",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "cross-player",
"version": "1.2.4",
"version": "1.2.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cross-player",
"version": "1.2.4",
"version": "1.2.5",
"license": "MIT",
"dependencies": {
"@types/sortablejs": "^1.15.9",

View file

@ -1,6 +1,6 @@
{
"name": "cross-player",
"version": "1.2.4",
"version": "1.2.5",
"description": "Media player plugin for Obsidian",
"main": "main.js",
"scripts": {

View file

@ -62,6 +62,9 @@ export default class CrossPlayerPlugin extends Plugin {
debouncedUpdateDeviceStatus: any;
lastSyncIssueKey: string = '';
lastKnownDataMtime: number = 0;
lastKnownDataSize: number = 0;
isReloadingSyncedData: boolean = false;
getLastGoodWatchedFolder(): string {
try {
@ -105,6 +108,53 @@ export default class CrossPlayerPlugin extends Plugin {
}
}
getPluginDataPath() {
return `${this.manifest.dir}/data.json`;
}
async getPluginDataStat() {
try {
return await this.app.vault.adapter.stat(this.getPluginDataPath());
} catch (error) {
console.warn('[Cross Player] Failed to stat data.json', error);
return null;
}
}
async refreshTrackedDataFileState() {
const stat = await this.getPluginDataStat();
this.lastKnownDataMtime = stat?.mtime ?? 0;
this.lastKnownDataSize = stat?.size ?? 0;
}
async reloadSyncedDataIfChanged(force: boolean = false) {
if (this.isReloadingSyncedData) return;
const stat = await this.getPluginDataStat();
const nextMtime = stat?.mtime ?? 0;
const nextSize = stat?.size ?? 0;
const changed = force || nextMtime !== this.lastKnownDataMtime || nextSize !== this.lastKnownDataSize;
if (!changed) return;
this.isReloadingSyncedData = true;
try {
await this.loadData();
await this.validatePluginSyncHealth();
await this.refreshTrackedDataFileState();
if (this.mainView) {
this.mainView.handleSyncedDataReload();
}
if (this.listView) {
this.listView.refresh();
}
} finally {
this.isReloadingSyncedData = false;
}
}
async validatePluginSyncHealth() {
try {
const adapter = this.app.vault.adapter;
@ -178,6 +228,7 @@ export default class CrossPlayerPlugin extends Plugin {
async onload() {
await this.loadData();
await this.refreshTrackedDataFileState();
await this.validatePluginSyncHealth();
this.calculateDynamicLimit();
@ -379,8 +430,7 @@ export default class CrossPlayerPlugin extends Plugin {
id: 'reload-data',
name: 'Reload Data from Disk',
callback: async () => {
await this.loadData();
if (this.listView) this.listView.refresh();
await this.reloadSyncedDataIfChanged(true);
new Notice("Data reloaded.");
}
});
@ -421,14 +471,12 @@ export default class CrossPlayerPlugin extends Plugin {
}
});
this.debouncedReload = debounce(async () => {
await this.reloadSyncedDataIfChanged();
}, 1000, true);
// Setup auto-reload for Desktop (handle Sync)
if (Platform.isDesktop) {
this.debouncedReload = debounce(async () => {
await this.loadData();
await this.validatePluginSyncHealth();
if (this.listView) this.listView.refresh();
}, 1000, true);
try {
const path = require('path');
const fs = require('fs');
@ -457,6 +505,14 @@ export default class CrossPlayerPlugin extends Plugin {
this.registerDomEvent(window, 'focus', () => {
this.debouncedReload();
});
this.registerDomEvent(document, 'visibilitychange', () => {
if (document.visibilityState === 'visible') {
this.debouncedReload();
}
});
this.registerInterval(window.setInterval(() => {
this.debouncedReload();
}, 5000));
this.registerWatchers();
@ -600,6 +656,7 @@ export default class CrossPlayerPlugin extends Plugin {
// If I just rely on DEFAULT_SETTINGS, existing users won't see a change if they have saved data.
// I will trust that `loaded.playbackSpeed` is what the user *last used*.
// If it's missing, it defaults to settings.defaultPlaybackSpeed.
await this.refreshTrackedDataFileState();
}
getTodayStatKey(): string {
@ -675,6 +732,7 @@ export default class CrossPlayerPlugin extends Plugin {
async saveData(refresh: boolean = true) {
this.rememberQueueScrollPosition();
await super.saveData(this.data);
await this.refreshTrackedDataFileState();
if (refresh && this.listView) this.listView.refresh();
}
@ -2210,8 +2268,7 @@ class CrossPlayerListView extends ItemView {
setIcon(refreshBtn, "refresh-cw");
refreshBtn.ariaLabel = "Refresh Data";
refreshBtn.onclick = async () => {
await this.plugin.loadData();
this.refresh();
await this.plugin.reloadSyncedDataIfChanged(true);
new Notice("Data reloaded.");
};
@ -2770,6 +2827,67 @@ class CrossPlayerMainView extends ItemView {
}
}
async persistPlaybackSnapshotOnClose() {
if (!this.currentItem) return;
const queueItem = this.plugin.data.queue.find(item => item.id === this.currentItem?.id)
|| this.plugin.data.queue.find(item => item.path === this.currentItem?.path);
if (!queueItem) return;
const currentTime = this.videoEl && isFinite(this.videoEl.currentTime)
? this.videoEl.currentTime
: (queueItem.position || 0);
const duration = this.videoEl && isFinite(this.videoEl.duration) && this.videoEl.duration > 0
? this.videoEl.duration
: (queueItem.duration || 0);
queueItem.position = currentTime;
this.currentItem.position = currentTime;
if (duration > 0 && Math.abs((queueItem.duration || 0) - duration) > 1) {
queueItem.duration = duration;
}
this.currentItem.duration = queueItem.duration;
const progress = duration > 0 ? currentTime / duration : 0;
if (progress > 0.95) {
queueItem.status = 'completed';
queueItem.finished = true;
this.plugin.recordConsumption(queueItem);
this.currentItem.status = 'completed';
this.currentItem.finished = true;
} else if (queueItem.status === 'playing') {
queueItem.status = queueItem.finished ? 'completed' : 'pending';
this.currentItem.status = queueItem.status;
}
await this.plugin.saveData();
}
handleSyncedDataReload() {
if (!this.currentItem) return;
const syncedItem = this.plugin.data.queue.find(item => item.id === this.currentItem?.id)
|| this.plugin.data.queue.find(item => item.path === this.currentItem?.path);
if (!syncedItem) return;
const isActivelyPlayingLocally = !!this.videoEl && this.isCurrentPlaybackSource() && !this.videoEl.paused && !this.videoEl.ended;
this.currentItem = syncedItem;
// When this device is not actively playing, trust the synced position.
if (this.videoEl && !isActivelyPlayingLocally && isFinite(this.videoEl.duration) && this.videoEl.duration > 0) {
const targetPosition = Math.max(0, Math.min(this.videoEl.duration, syncedItem.position || 0));
if (Math.abs(this.videoEl.currentTime - targetPosition) > 1) {
this.videoEl.currentTime = targetPosition;
}
}
this.updateOverlayProgress();
if (this.plugin.listView) {
this.plugin.listView.updateStats();
}
}
async onOpen() {
const container = this.contentEl;
container.empty();
@ -2924,26 +3042,13 @@ class CrossPlayerMainView extends ItemView {
}
async onClose() {
const closingItem = this.currentItem;
const shouldResetStatus = closingItem?.status === 'playing';
if (this.videoEl) {
this.videoEl.pause();
await this.syncCurrentItemDuration();
await this.persistCurrentPlaybackPosition(true);
await this.syncCompletionStatusFromPlayback();
await this.persistPlaybackSnapshotOnClose();
this.videoEl.removeAttribute('src');
this.videoEl.load();
}
if (closingItem && shouldResetStatus) {
if (closingItem.finished) {
await this.plugin.updateStatus(closingItem.id, 'completed');
} else {
await this.plugin.updateStatus(closingItem.id, 'pending');
}
}
this.activeMediaSrc = null;
this.currentItem = null;
this.lastPositionPersist = 0;