mirror of
https://github.com/imed-ghomari/cross-player-obsidian.git
synced 2026-07-22 06:55:49 +00:00
Release 1.2.5
This commit is contained in:
parent
4e8385ad07
commit
f3ecda4ac7
6 changed files with 262 additions and 51 deletions
9
RELEASE_NOTES_1.2.5.md
Normal file
9
RELEASE_NOTES_1.2.5.md
Normal 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
143
main.js
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
153
src/main.ts
153
src/main.ts
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue