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
2 changes: 1 addition & 1 deletion src/app/feed/feed.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class FeedPage {
async openSettings() {
const settings = await this.modalController.create({
component: SettingsComponent,
breakpoints: [0, 0.4, 0.7],
breakpoints: [0, 0.4, 0.8, 1.0],
initialBreakpoint: 0.4
});

Expand Down
4 changes: 4 additions & 0 deletions src/app/services/feed.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,12 @@ export class FeedService implements OnDestroy {

public filterArticles(array: Array<any>, settings: ISettingsDict): Array<any> {
const muted = settings.mutedWords.map(w => w.toLowerCase());
const highlighted = (settings.highlightedWords || []).map(w => w.toLowerCase());
return array.filter(a => {
const content = (a.title + ' ' + a.content).toLowerCase();
// Determine highlight status
a.highlighted = highlighted.some(word => content.includes(word));
// Determine mute status
return !muted.some(word => content.includes(word));
});
}
Expand Down
64 changes: 53 additions & 11 deletions src/app/services/settings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ const SETTINGS_DICT = 'v3_settings_dict';

export interface ISettingsDict {
preview: boolean,
showImages: boolean,
showImages: 'never' | 'whenHighlighted' | 'always',
showSnippet: 'never' | 'whenHighlighted' | 'always',
compressedFeed: boolean,
locale: string,
retrievalTimeout: number,
defaultPollingFrequency: number,
maxFeedLength: number,
mutedWords: Array<string>
mutedWords: Array<string>,
highlightedWords: Array<string>
}

@Injectable({
Expand All @@ -20,13 +22,15 @@ export interface ISettingsDict {
export class SettingsService {
private _settingsDict: ISettingsDict = {
preview: true,
showImages: false,
showImages: 'whenHighlighted',
showSnippet: 'whenHighlighted',
compressedFeed: false,
locale: 'en-AU',
retrievalTimeout: 5000, // 5 seconds
defaultPollingFrequency: 0, // Unlimited
maxFeedLength: 10,
mutedWords: []
mutedWords: [],
highlightedWords: []
};

constructor(private storageService: StorageService) {
Expand All @@ -45,10 +49,47 @@ export class SettingsService {
}
}

public updateSettings(settings: ISettingsDict): void {
this._settingsDict = settings;
/**
* Normalize incoming settings (merge with defaults and support old formats)
*/
private normalizeSettings(incoming: Partial<ISettingsDict> | unknown): ISettingsDict {
const merged: ISettingsDict = { ...this._settingsDict, ...(incoming as Partial<ISettingsDict> || {}) } as ISettingsDict;

// Backwards compatibility for showImages: older versions used boolean
const rawShow = (incoming as Partial<Record<string, unknown>>)?.['showImages'];
if (typeof rawShow === 'boolean') {
merged.showImages = rawShow ? 'always' : 'never';
} else if (typeof rawShow === 'string') {
const allowed = ['never', 'whenHighlighted', 'always'];
merged.showImages = allowed.includes(rawShow) ? (rawShow as ISettingsDict['showImages']) : this._settingsDict.showImages;
} else {
// keep default/merged
merged.showImages = (merged.showImages as ISettingsDict['showImages']) ?? this._settingsDict.showImages;
}

// Backwards compatibility for showSnippet: older versions may have used boolean or not existed
const rawSnippet = (incoming as Partial<Record<string, unknown>>)?.['showSnippet'];
if (typeof rawSnippet === 'boolean') {
merged.showSnippet = rawSnippet ? 'always' : 'never';
} else if (typeof rawSnippet === 'string') {
const allowedS = ['never', 'whenHighlighted', 'always'];
merged.showSnippet = allowedS.includes(rawSnippet) ? (rawSnippet as ISettingsDict['showSnippet']) : this._settingsDict.showSnippet;
} else {
merged.showSnippet = (merged.showSnippet as ISettingsDict['showSnippet']) ?? this._settingsDict.showSnippet;
}

// Ensure arrays exist
merged.mutedWords = (merged.mutedWords ?? []) as string[];
merged.highlightedWords = (merged.highlightedWords ?? []) as string[];

return merged;
}

public updateSettings(settings: Partial<ISettingsDict> | ISettingsDict): void {
// Normalize to handle older settings formats and missing fields
this._settingsDict = this.normalizeSettings(settings as Partial<ISettingsDict>);
this.storageService.set(SETTINGS_DICT, this._settingsDict);
console.log(`[SettingsService] Updated settings: ${this._settingsDict}`);
console.log(`[SettingsService] Updated settings: ${JSON.stringify(this._settingsDict)}`);
}

public getSettings() {
Expand All @@ -57,10 +98,11 @@ export class SettingsService {

public async loadSettingsDict() {
console.log('[SettingsService] Loading settings from storage');
const storage_feed = await this.storageService.getObjectFromStorage(SETTINGS_DICT);

if (storage_feed !== null) {
this._settingsDict = storage_feed;
const current_settings = await this.storageService.getObjectFromStorage(SETTINGS_DICT);

if (current_settings !== null) {
// Merge/normalize stored settings into defaults
this._settingsDict = this.normalizeSettings(current_settings as Partial<ISettingsDict>);
}
else
this.storageService.set(SETTINGS_DICT, this._settingsDict);
Expand Down
18 changes: 16 additions & 2 deletions src/app/settings/settings.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,18 @@ <h1>Preferences</h1>
(ionChange)="update()"> Show entry preview </ion-toggle>
</ion-item>
<ion-item>
<ion-toggle checked="currentSettings.showImages" [(ngModel)]="currentSettings.showImages"
(ionChange)="update()"> Show entry images </ion-toggle>
<ion-select label="Show entry image" [(ngModel)]="currentSettings.showImages" (ionChange)="update()">
<ion-select-option value="never">Never</ion-select-option>
<ion-select-option value="whenHighlighted">When Highlighted</ion-select-option>
<ion-select-option value="always">Always</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-select label="Show entry snippet" [(ngModel)]="currentSettings.showSnippet" (ionChange)="update()">
<ion-select-option value="never">Never</ion-select-option>
<ion-select-option value="whenHighlighted">When Highlighted</ion-select-option>
<ion-select-option value="always">Always</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-toggle checked="currentSettings.compressedFeed" [(ngModel)]="currentSettings.compressedFeed"
Expand Down Expand Up @@ -54,5 +64,9 @@ <h1>Preferences</h1>
<ion-label>Muted Words</ion-label>
<ion-note slot="end">{{ (currentSettings.mutedWords || []).length }}</ion-note>
</ion-item>
<ion-item button (click)="openHighlightedWordsDialog()">
<ion-label>Highlighted Words</ion-label>
<ion-note slot="end">{{ (currentSettings.highlightedWords || []).length }}</ion-note>
</ion-item>
</ion-list>
</ion-content>
55 changes: 55 additions & 0 deletions src/app/settings/settings.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,61 @@ export class SettingsComponent implements OnInit {
await alert.present();
}

async openHighlightedWordsDialog() {
const alert = await this.alertController.create({
header: 'Highlighted Words',
inputs: this.currentSettings.highlightedWords.map(word => ({
name: word,
type: 'checkbox',
label: word,
value: word,
checked: true,
})),
buttons: [
{
text: 'Add New',
handler: async () => {
const prompt = await this.alertController.create({
header: 'Add Highlighted Word',
inputs: [
{
name: 'newWord',
type: 'text',
placeholder: 'e.g., important'
}
],
buttons: [
{ text: 'Cancel', role: 'cancel' },
{
text: 'Add',
handler: (data) => {
if (data.newWord?.trim()) {
this.currentSettings.highlightedWords.push(data.newWord.trim().toLowerCase());
this.settingsService.updateSettings(this.currentSettings);
}
}
}
]
});
await prompt.present();
}
},
{
text: 'Remove Selected',
handler: (selected: string[]) => {
this.currentSettings.highlightedWords = this.currentSettings.highlightedWords.filter(w => !selected.includes(w));
this.settingsService.updateSettings(this.currentSettings);
}
},
{
text: 'Cancel',
role: 'cancel'
}
]
});
await alert.present();
}

// Getter/Setters for settings stored as numbers to ensure types are correct
get retrievalTimeout() { return String(this.currentSettings.retrievalTimeout); }
set retrievalTimeout(v) { this.currentSettings.retrievalTimeout = Number(v); }
Expand Down
25 changes: 16 additions & 9 deletions src/app/shared/article-list/article-list.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<div>
@if (!settingsService.getSettings().compressedFeed) {
<div>
<ion-card [button]="true" (click)="openPreview(entry.title, entry.content, entry.link)">
@if (settingsService.getSettings().showImages) {
<ion-card [button]="true" (click)="openPreview(entry.title, entry.content, entry.link)" [class.highlighted]="entry.highlighted">
@if (settingsService.getSettings().showImages === 'always' || (settingsService.getSettings().showImages === 'whenHighlighted' && entry.highlighted)) {
<div class="card-image">
@if (entry.imgLink !== null) {
<img alt="Entry Cover Image" src="{{ entry.imgLink }}" />
Expand All @@ -19,7 +19,10 @@
{{entry.source}} | {{formatDateRelative(entry.isoDate, settingsService.getSettings().locale)}}
</ion-card-subtitle>
<ion-card-title>
<ion-item class="ion-no-padding" style="--ion-item-background: transparent;">
<ion-item
[lines]="(settingsService.getSettings().showSnippet === 'always' || (settingsService.getSettings().showSnippet === 'whenHighlighted' && entry.highlighted)) ? 'full' : 'none'"
class="ion-no-padding"
style="--ion-item-background: transparent;">
<ion-label>{{entry.title}}</ion-label>
@if (entry.bookmark !== true) {
<ion-button slot="end" fill="clear" (click)="this.addBookmark($event, entry)">
Expand All @@ -34,16 +37,18 @@
</ion-item>
</ion-card-title>
</ion-card-header>
<ion-card-content>
{{entry.contentStripped}}
</ion-card-content>
@if (settingsService.getSettings().showSnippet === 'always' || (settingsService.getSettings().showSnippet === 'whenHighlighted' && entry.highlighted)) {
<ion-card-content>
{{entry.contentStripped}}
</ion-card-content>
}
</ion-card>
</div>
}
@if (settingsService.getSettings().compressedFeed) {
<div>
<ion-item [button]="true" (click)="openPreview(entry.title, entry.content, entry.link)">
@if (settingsService.getSettings().showImages) {
<ion-item [button]="true" (click)="openPreview(entry.title, entry.content, entry.link)" [class.highlighted]="entry.highlighted">
@if (settingsService.getSettings().showImages === 'always' || (settingsService.getSettings().showImages === 'whenHighlighted' && entry.highlighted)) {
<div>
@if (entry.imgLink !== null) {
<ion-thumbnail slot="start">
Expand All @@ -55,7 +60,9 @@
<ion-label>
<strong>{{entry.title}}</strong><br />
<ion-text>{{entry.source}}</ion-text><br />
<ion-note color="medium" class="ion-text-wrap">{{entry.contentStripped}}</ion-note>
@if (settingsService.getSettings().showSnippet === 'always' || (settingsService.getSettings().showSnippet === 'whenHighlighted' && entry.highlighted)) {
<ion-note color="medium" class="ion-text-wrap">{{entry.contentStripped}}</ion-note>
}
</ion-label>
<div class="metadata-end-wrapper" slot="end">
<ion-note color="medium">{{formatDateAsDay(entry.isoDate, settingsService.getSettings().locale)}}</ion-note>
Expand Down
10 changes: 10 additions & 0 deletions src/app/shared/article-list/article-list.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,13 @@
flex-direction: column;
align-items: flex-end;
}

.highlighted {
border: 2px solid #FFD54F; /* amber-300 */
box-shadow: 0 0 6px rgba(255, 213, 79, 0.3);
}

/* ensure card-image still looks fine when highlighted */
.highlighted .card-image img {
max-width: 100%;
}