Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ A simple desktop RSS reader application for Mac built with Electron. The applica
- **OpenAI API key management** - Secure, encrypted storage of API keys using AES-256-GCM
- **Feature flags** - Toggle individual features on/off to customize your experience
- **Settings modal** - Centralized configuration interface accessible from the sidebar
- **Data Mirror with Markdown sync** - Mirror all RSS data to an external directory, with option to convert to Markdown files with Obsidian-compatible frontmatter

![Settings](screenshots/settings.png)

Expand All @@ -47,6 +48,7 @@ A simple desktop RSS reader application for Mac built with Electron. The applica
- **Automatic updates** - Background update checking and installation via electron-updater
- **Data migration** - Automatic data structure migrations when updating to new versions
- **Data persistence** - All feeds, articles, settings, and read states are stored locally
- **Data Mirror** - Mirror all data to a second directory for integration with personal knowledge management tools like Obsidian (requires feature flag)

### Coming Soon
- OPML import/export support
Expand Down Expand Up @@ -174,9 +176,63 @@ To configure your API key:
The Settings area includes a feature flag system that allows you to enable or disable specific features:

- **AI Article Summary**: Enable AI-powered article summaries
- **Data Mirror**: Enable mirroring of all data to a second directory

Feature flags can be toggled on or off using the toggle switches in the Settings modal. Changes are saved immediately when you click "Save".

#### Data Mirror

When the Data Mirror feature flag is enabled, a new section appears in Settings that allows you to:

1. **Select a Mirror Directory**: Click "Browse..." to choose a directory where all RSS data will be mirrored
2. **Sync as Markdown** (enabled by default): Toggle to convert data to Markdown files with Obsidian-compatible frontmatter instead of raw JSON files
3. **Clear Directory**: Click the "✕" button to clear the selected directory
4. **Sync Now**: Manually trigger a sync of all data to the mirror directory

The data is automatically synced to the mirror directory whenever any data changes (feeds, articles, read states, settings, etc.). This is useful for integration with personal knowledge management tools like Obsidian, Notion, or any other tool that can read Markdown or JSON files.

**Note:** The "Sync as Markdown" option is enabled by default, providing a more readable and organized format for your mirrored data. You can disable it to mirror raw JSON files instead.

**Sync as Markdown Mode (Default):**

When "Sync as Markdown" is enabled (the default), the mirrored data is organized into a clean folder structure and converted to Markdown files with [Obsidian-compatible frontmatter properties](https://help.obsidian.md/properties):

- `articles/{feed-name}/` - Individual article files, one per article, with full content and metadata
- `feeds/` - Feed subscription files with URL, status, category, and update information
- `categories/` - Category/folder definition files

This format makes it easy to:
- Browse and search articles in your knowledge management tool
- Link between articles, feeds, and categories
- Use Obsidian's graph view to visualize your reading patterns
- Export or backup your RSS data in a human-readable format

Each Markdown file includes YAML frontmatter with properties like:
```yaml
---
title: "Article Title"
link: "https://example.com/article"
author: "Author Name"
feed: "Feed Name"
pubDate: "2024-01-15T10:30:00Z"
read: true
tags:
- "Technology"
- "News"
type: article
---
```

**JSON Mode:**

When "Sync as Markdown" is disabled, the following JSON files are mirrored directly:
- `feeds.json` - Feed subscriptions
- `categories.json` - Category/folder organization
- `read-states.json` - Article read status
- `settings.json` - Application settings
- `ai-summaries.json` - AI-generated summaries (if enabled)
- `articles-{feedId}.json` - Articles for each feed

### Feed Management

The Feed Management page allows you to:
Expand Down
122 changes: 122 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,33 @@ <h3 class="modal-title">Settings</h3>
<!-- Feature flags will be dynamically rendered here -->
</div>
</div>
<div class="settings-section" id="dataMirrorSection" style="display: none;">
<div class="settings-section-title">Data Mirror</div>
<div class="form-group">
<label class="form-label">Mirror Directory</label>
<div style="display: flex; gap: 10px; align-items: center;">
<input type="text" class="form-input" id="mirrorDirectoryInput" readonly placeholder="No directory selected" style="flex: 1;">
<button class="btn btn-secondary" id="selectMirrorDirectoryBtn">Browse...</button>
<button class="btn btn-secondary" id="clearMirrorDirectoryBtn" title="Clear directory">✕</button>
</div>
<p class="form-help">Select a directory to mirror all RSS data. Useful for integration with personal knowledge management tools like Obsidian.</p>
</div>
<div class="form-group" style="margin-top: 16px;">
<div class="feature-flag-item">
<div class="feature-flag-info">
<div class="feature-flag-name">Sync as Markdown (.md)</div>
<div class="feature-flag-description">Convert data to Markdown files with Obsidian-compatible frontmatter properties instead of JSON</div>
</div>
<label class="toggle-switch">
<input type="checkbox" id="syncAsMarkdownToggle" checked>
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div style="margin-top: 12px;">
<button class="btn btn-secondary" id="syncNowBtn">Sync Now</button>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancelSettingsBtn">Cancel</button>
Expand Down Expand Up @@ -1965,6 +1992,26 @@ <h1 class="article-preview-title">${escapeHtml(article.title || 'Untitled')}</h1
renderFeatureFlags(result.data);
}
}

// Load mirror directory
if (window.electronAPI && window.electronAPI.getMirrorDirectory) {
const result = await window.electronAPI.getMirrorDirectory();
if (result.success && result.data) {
const mirrorInput = document.getElementById('mirrorDirectoryInput');
if (mirrorInput) {
mirrorInput.value = result.data;
}
}
}

// Load syncAsMarkdown setting (defaults to true)
if (window.electronAPI && window.electronAPI.getSyncAsMarkdown) {
const result = await window.electronAPI.getSyncAsMarkdown();
const syncAsMarkdownToggle = document.getElementById('syncAsMarkdownToggle');
if (syncAsMarkdownToggle) {
syncAsMarkdownToggle.checked = result.success ? result.data : true;
}
}
} catch (error) {
console.error('Error loading settings:', error);
}
Expand All @@ -1976,6 +2023,11 @@ <h1 class="article-preview-title">${escapeHtml(article.title || 'Untitled')}</h1
name: 'aiArticleSummary',
label: 'AI Article Summary',
description: 'Display AI-generated article summaries above the article preview (requires OpenAI API key)'
},
{
name: 'dataMirror',
label: 'Data Mirror',
description: 'Mirror all RSS data to a second directory for integration with external tools (e.g., Obsidian)'
}
];

Expand All @@ -1994,6 +2046,12 @@ <h1 class="article-preview-title">${escapeHtml(article.title || 'Untitled')}</h1
</div>
`;
}).join('');

// Show/hide data mirror section based on feature flag
const dataMirrorSection = document.getElementById('dataMirrorSection');
if (dataMirrorSection) {
dataMirrorSection.style.display = featureFlags.dataMirror ? 'block' : 'none';
}
}

async function saveSettings() {
Expand Down Expand Up @@ -2022,6 +2080,25 @@ <h1 class="article-preview-title">${escapeHtml(article.title || 'Untitled')}</h1
}
}
}

// Save mirror directory
const mirrorInput = document.getElementById('mirrorDirectoryInput');
if (mirrorInput && window.electronAPI && window.electronAPI.setMirrorDirectory) {
const mirrorDirectory = mirrorInput.value.trim() || null;
const result = await window.electronAPI.setMirrorDirectory(mirrorDirectory);
if (!result.success) {
console.error('Failed to save mirror directory:', result.error);
}
}

// Save syncAsMarkdown setting
const syncAsMarkdownToggle = document.getElementById('syncAsMarkdownToggle');
if (syncAsMarkdownToggle && window.electronAPI && window.electronAPI.setSyncAsMarkdown) {
const result = await window.electronAPI.setSyncAsMarkdown(syncAsMarkdownToggle.checked);
if (!result.success) {
console.error('Failed to save syncAsMarkdown setting:', result.error);
}
}

// Reload feature flags to update UI immediately
await loadFeatureFlags();
Expand Down Expand Up @@ -2092,6 +2169,51 @@ <h1 class="article-preview-title">${escapeHtml(article.title || 'Untitled')}</h1
closeSettingsModal();
}
});

// Data Mirror event handlers
document.getElementById('selectMirrorDirectoryBtn').addEventListener('click', async () => {
if (window.electronAPI && window.electronAPI.selectMirrorDirectory) {
try {
const result = await window.electronAPI.selectMirrorDirectory();
if (result.success && result.data) {
document.getElementById('mirrorDirectoryInput').value = result.data;
}
} catch (error) {
console.error('Error selecting mirror directory:', error);
showToast('Failed to select directory');
}
}
});

document.getElementById('clearMirrorDirectoryBtn').addEventListener('click', () => {
document.getElementById('mirrorDirectoryInput').value = '';
});

document.getElementById('syncNowBtn').addEventListener('click', async () => {
if (window.electronAPI && window.electronAPI.syncToMirror) {
try {
const result = await window.electronAPI.syncToMirror();
if (result.success) {
showToast('Data synced to mirror directory');
} else {
showToast('Sync failed: ' + (result.error || 'Unknown error'));
}
} catch (error) {
console.error('Error syncing to mirror:', error);
showToast('Sync failed');
}
}
});

// Listen for feature flag changes to show/hide data mirror section
featureFlagList.addEventListener('change', (e) => {
if (e.target.dataset.flagName === 'dataMirror') {
const dataMirrorSection = document.getElementById('dataMirrorSection');
if (dataMirrorSection) {
dataMirrorSection.style.display = e.target.checked ? 'block' : 'none';
}
}
});

// Update Notification Management
const updateNotification = document.getElementById('updateNotification');
Expand Down
Loading