Skip to content

Commit 53b5767

Browse files
committed
Add persistent settings (via chrome.storage.sync).
1 parent 804690e commit 53b5767

13 files changed

+475
-12
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
22

3+
src/schemas/*.validator.*
4+
35
# Compiled output
46
/dist
57
/tmp

custom-webpack.config.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
11
// see https://www.npmjs.com/package/@angular-builders/custom-webpack
22
import type { Configuration } from 'webpack';
33

4+
// Note: AJV transforms/compiles a JSON Schema to an actual JS function. You can then call that function
5+
// to validate input against said schema. BUT, to generate the function AJV uses dynamic code
6+
// evaluation, which means doing this at runtime exposes us to the risk of code injection. To avoid
7+
// this (and the need to add 'unsafe-eval' to our CSP in manifest.json) we use webpack's compile hook
8+
// hook to "pre-compile" our validator function(s).
9+
import { compile } from './utils/ajv-utils';
10+
411
export default {
512
entry: {
613
background: 'src/background.ts',
714
content: 'src/content.ts'
815
},
16+
plugins: [
17+
{
18+
apply: (compiler) => {
19+
compiler.hooks.compile.tap("AjvPlugin", (_params) => {
20+
compile( { schema: "src/schemas/settings.json", useDefaults: true } );
21+
});
22+
},
23+
},
24+
],
925
optimization: {
1026
runtimeChunk: false
1127
},

package-lock.json

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@types/jasmine": "~5.1.0",
4040
"autoprefixer": "^10.4.20",
4141
"jasmine-core": "~5.2.0",
42+
"json-schema-typed": "^8.0.1",
4243
"karma": "~6.4.0",
4344
"karma-chrome-launcher": "~3.2.0",
4445
"karma-coverage": "~2.2.0",

src/app/settings.service.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { TestBed } from '@angular/core/testing';
2+
3+
import { SettingsService } from './settings.service';
4+
5+
describe('SettingsService', () => {
6+
let service: SettingsService;
7+
8+
beforeEach(() => {
9+
TestBed.configureTestingModule({});
10+
service = TestBed.inject(SettingsService);
11+
});
12+
13+
it('should be created', () => {
14+
expect(service).toBeTruthy();
15+
});
16+
});

src/app/settings.service.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Angular service wrapping Electron IPC for user settings.
2+
3+
// 2024-06-09 - Shaun L. Cloherty <[email protected]>
4+
5+
import { Injectable } from '@angular/core';
6+
7+
import _settings from '../settings';
8+
9+
import { Setting } from '../settings';
10+
export type { Setting } from '../settings';
11+
12+
@Injectable({
13+
providedIn: 'root'
14+
})
15+
export class SettingsService {
16+
// key: string | null = null;
17+
18+
constructor() { }
19+
20+
// lock (): void {
21+
// // console.log('SettingsService.lock()');
22+
// // 1. clear this.settings
23+
// // 2. discard key
24+
// this.key = null;
25+
// // 3. clear retriever in the service worker?
26+
// }
27+
28+
// unlock (passphrase?: string): Promise<void> {
29+
// // console.log('SettingsService.unlock():', passphrase);
30+
// return new Promise((resolve, _reject) => {
31+
// // 1. generate key from passphrase
32+
// this.key = passphrase? passphrase : null;
33+
// // 2. retrieve settings from chrome.storage.sync
34+
// // 3. decrypt settings using key
35+
// resolve();
36+
// });
37+
// }
38+
39+
get (name?: string): Promise<Setting[]> {
40+
// console.log('SettingsService.get()', name);
41+
// return new Promise((resolve, reject) => {
42+
// this.settings && this.key ?
43+
// // resolve(name ? this.settings.filter((setting) => setting.name === name) : this.settings) :
44+
// resolve(name ? [ _get(name) as Setting ] : _get() as Setting[]) :
45+
// reject(Error('Failed to load settings!')) // locked?
46+
// });
47+
return name ? _settings.get(name) as Promise<Setting[]> : _settings.get() as Promise<Setting[]>;
48+
}
49+
50+
set (settings: Setting[]): void {
51+
// console.log('SettingsService.set()', setting);
52+
_settings.set(settings);
53+
}
54+
}

src/app/settings/settings.component.html

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,32 @@
1515
</a>
1616
</div>
1717
</div>
18-
<div class="w-full justify-items-center bg-slate-100 grow">
19-
<div class="text-slate-400 py-1 uppercase">
20-
<a routerLink="/" routerLinkActive="active" ariaCurrentWhenActive="page">Home</a> | Settings
18+
19+
@if (settings !== null) {
20+
<!-- <div class="absolute z-10 min-w-full px-12 flex flex-col top-4"> -->
21+
<div class="min-w-full px-2 py-2 bg-slate-100 flex flex-col grow">
22+
23+
<ul>
24+
@for (setting of settings ; track setting.label) {
25+
<li class="flex flex-col my-2">
26+
<p>{{setting.label}}</p>
27+
<span class="font-light text-gray-500 text-sm">{{setting.description}}</span>
28+
<input class="p-2 placeholder:text-slate-300 outline outline-1 outline-slate-200 focus:outline-cyan-500 rounded-sm" type="text" autocomplete="off" [(ngModel)]="setting.value" name="{{setting.value}}" ngDefaultControl placeholder="{{setting.label}}">
29+
</li>
30+
}
31+
</ul>
32+
<!-- <div class="flex flex-col justify-end grow"> -->
33+
<div class="flex flex-row gap-2 place-self-end justify-end">
34+
<input type="button" class="px-4 py-2 bg-cyan-500 text-white rounded-md" value="Apply" (click)="onClick(true)">
35+
<input type="button" class="px-4 py-2 bg-slate-200 text-slate-500 rounded-md" value="Cancel" (click)="onClick(false)">
36+
</div>
37+
<!-- </div> -->
2138
</div>
22-
</div>
23-
</div>
39+
}
40+
@else {
41+
<div class="min-w-full px-2 py-2 bg-slate-100 flex flex-col grow">
42+
<input #passphrase type="password" class="p-2 placeholder:text-slate-300 outline outline-1 outline-slate-200 focus:outline-cyan-500 rounded-sm" [(ngModel)]="passphrase" name="passphrase" ngDefaultControl placeholder="Passphrase">
43+
<input type="button" class="px-4 py-2 bg-cyan-500 text-white rounded-md" value="Unlock" (click)="unlock(passphrase.value)">
44+
</div>
45+
}
46+
</div>
Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,52 @@
1-
import { Component } from '@angular/core';
2-
import { RouterOutlet, RouterLink } from '@angular/router';
1+
import { Component, OnInit } from '@angular/core';
2+
import { FormsModule } from '@angular/forms';
3+
import { Router, RouterLink } from '@angular/router';
4+
5+
import { SettingsService, Setting } from '../settings.service';
36

47
@Component({
58
selector: 'app-settings',
69
standalone: true,
7-
imports: [RouterOutlet, RouterLink],
10+
imports: [FormsModule, RouterLink],
811
templateUrl: './settings.component.html',
912
styleUrl: './settings.component.css'
1013
})
11-
export class SettingsComponent {
14+
export class SettingsComponent implements OnInit {
15+
settings: Setting[] | null = null;
16+
17+
constructor( private router: Router, private settingsService: SettingsService) {
18+
// injects SettingsService as this.settingsService
19+
console.log('SettingsComponent.constructor()');
20+
}
21+
22+
ngOnInit() {
23+
console.log('SettingsComponent.ngOnInit()');
24+
this.settingsService.get()
25+
.then(
26+
(settings: Setting[]) => {this.settings = settings}
27+
)
28+
.catch(
29+
(error) => { this.settings = null; console.error('SettingsComponent.ngOnInit()', error); }
30+
);
31+
}
32+
33+
onClick(apply: boolean) {
34+
if (apply ) {
35+
// apply the settings via the settings service
36+
this.settingsService.set(this.settings? this.settings : []);
37+
}
38+
39+
this.router.navigate(['/']);
40+
}
41+
42+
unlock(passphrase: string) {
43+
// unlock settings
44+
// this.settingsService.unlock(passphrase);
1245

46+
// const url = this.router.url
47+
// this.router.navigateByUrl('/', { skipLocationChange: true })
48+
// .then(() => {
49+
// this.router.navigate([`${url}`]);
50+
// });
51+
}
1352
}

src/background.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ chrome.runtime.onMessage.addListener(function (message, _sender, sendResponse) {
9191
});
9292

9393

94+
chrome.storage.onChanged.addListener((changes, namespace) => {
95+
for (let [key, { oldValue, newValue }] of Object.entries(changes)) {
96+
console.log(
97+
`Storage key "${key}" in namespace "${namespace}" changed.`,
98+
`Old value was "${oldValue}", new value is "${newValue}".`
99+
);
100+
}
101+
});
102+
94103
// ------------------------------
95104

96105
// // create a new periodic alarm

src/retriever.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,12 @@ export class PineconeStoreNoContent
287287

288288
import { Pinecone as PineconeClient } from "@pinecone-database/pinecone";
289289

290-
import { PINECONE_API_KEY } from "./secrets";
291-
const pc = new PineconeClient({ apiKey: PINECONE_API_KEY });
290+
// import { PINECONE_API_KEY } from "./secrets";
291+
import settings from "./settings"
292+
import { Setting } from "./settings";
293+
294+
const apiKey = await settings.get("pinecone-api-key") as Setting;
295+
const pc = new PineconeClient({ apiKey: apiKey.value });
292296

293297
const namespace = ""; // default: empty namespace
294298
const index = pc.index("vhs-ext").namespace(namespace); // FIXME: .Index vs .index?
@@ -471,7 +475,7 @@ export async function search(query: string): Promise<Bookmark[]> {
471475
return Promise.resolve(results.map(doc => Bookmark.fromDocument(doc)));
472476
}
473477

474-
478+
export default { add, del, get, search };
475479

476480

477481

0 commit comments

Comments
 (0)