17 Commits

Author SHA1 Message Date
DEV Sam Hayes
53ec023218 firefox-0.0.3 2025-02-07 23:15:48 +01:00
DEV Sam Hayes
bd8bd101d7 prepare firefox-0.0.3 2025-02-07 23:15:16 +01:00
DEV Sam Hayes
e85ac5ca66 harmonize chrome ui 2025-02-07 22:28:29 +01:00
DEV Sam Hayes
0a77eceaf4 Merge branch 'feature/firefox' into develop 2025-02-07 22:26:56 +01:00
DEV Sam Hayes
6c43a60810 migrate background related things from chrome 2025-02-07 22:26:34 +01:00
DEV Sam Hayes
b20faf2359 "copy" UI related things from chrome 2025-02-07 20:20:15 +01:00
DEV Sam Hayes
601ac8cd49 Merge branch 'feature/chrome-options' into develop 2025-02-07 17:14:39 +01:00
DEV Sam Hayes
27e8d52d23 add new route "/vault-import" in the popup 2025-02-07 17:14:29 +01:00
DEV Sam Hayes
7f0829af09 add options page (to upload vault snapshots) 2025-02-07 17:13:50 +01:00
DEV Sam Hayes
3ec8827c27 hotfix wrong chrome version in package.json 2025-02-04 20:30:39 +01:00
DEV Sam Hayes
f2960743e1 hotfix wrong chrome version in package.json 2025-02-04 20:29:12 +01:00
DEV Sam Hayes
d3d76efc83 merge release changes back into develop 2025-02-04 20:25:02 +01:00
DEV Sam Hayes
4b7d8d4a8d chrome-0.0.2 2025-02-04 20:24:05 +01:00
DEV Sam Hayes
a973b1edad prepare chrome-0.0.2 2025-02-04 20:23:40 +01:00
DEV Sam Hayes
312a3666d7 Merge branch 'fix/chrome' into develop 2025-02-04 20:18:16 +01:00
DEV Sam Hayes
142b313867 remove unnecessary permission "activeTab" 2025-02-04 20:17:54 +01:00
DEV Sam Hayes
b21701f677 move startupService to common 2025-02-04 20:13:45 +01:00
135 changed files with 5116 additions and 459 deletions

View File

@@ -1 +1 @@
npm run lint
# npm run lint

View File

@@ -121,11 +121,14 @@
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"path": "projects/firefox/custom-webpack.config.ts"
},
"outputPath": "dist/firefox",
"index": "projects/firefox/src/index.html",
"browser": "projects/firefox/src/main.ts",
"main": "projects/firefox/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "projects/firefox/tsconfig.app.json",
"inlineStyleLanguage": "scss",
@@ -136,15 +139,15 @@
}
],
"styles": ["projects/firefox/src/styles.scss"],
"scripts": []
"scripts": ["node_modules/bootstrap/dist/js/bootstrap.bundle.js"]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
"maximumWarning": "5MB",
"maximumError": "10MB"
},
{
"type": "anyComponentStyle",
@@ -152,7 +155,11 @@
"maximumError": "8kB"
}
],
"outputHashing": "all"
"optimization": {
"scripts": true,
"styles": false,
"fonts": true
}
},
"development": {
"optimization": false,
@@ -192,6 +199,16 @@
"styles": ["projects/firefox/src/styles.scss"],
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"projects/firefox/**/*.ts",
"projects/firefox/**/*.html"
],
"eslintConfig": "projects/firefox/eslint.config.js"
}
}
}
},

7
firefox_prepare_manifest.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
version=$( cat package.json | jq '.custom.firefox.version' | tr -d '"')
jq '.version = $newVersion' --arg newVersion $version ./projects/firefox/public/manifest.json > ./projects/firefox/public/tmp.manifest.json && mv ./projects/firefox/public/tmp.manifest.json ./projects/firefox/public/manifest.json
echo $version

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "gooti-extension",
"version": "0.0.1",
"version": "0.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gooti-extension",
"version": "0.0.1",
"version": "0.0.3",
"dependencies": {
"@angular/animations": "^19.0.0",
"@angular/common": "^19.0.0",

View File

@@ -1,12 +1,12 @@
{
"name": "gooti-extension",
"version": "0.0.1",
"version": "0.0.3",
"custom": {
"chrome": {
"version": "0.0.1"
"version": "0.0.2"
},
"firefox": {
"version": "0.0.0"
"version": "0.0.3"
}
},
"scripts": {
@@ -16,10 +16,11 @@
"start:chrome": "ng serve chrome",
"start:firefox": "ng serve firefox",
"prepare:chrome": "./chrome_prepare_manifest.sh",
"prepare:firefox": "./firefox_prepare_manifest.sh",
"build:chrome": "npm run prepare:chrome && ng build chrome",
"build:firefox": "ng build firefox",
"build:firefox": "npm run prepare:firefox && ng build firefox",
"watch:chrome": "npm run prepare:chrome && ng build chrome --watch --configuration development",
"watch:firefox": "ng build firefox --watch --configuration development",
"watch:firefox": "npm run prepare:firefox && ng build firefox --watch --configuration development",
"test": "ng test",
"lint": "ng lint",
"prepare": "husky"

View File

@@ -18,5 +18,9 @@ module.exports = {
import: 'src/prompt.ts',
runtime: false,
},
options: {
import: 'src/options.ts',
runtime: false,
},
},
} as Configuration;

View File

@@ -2,10 +2,10 @@
"manifest_version": 3,
"name": "Gooti",
"description": "Nostr Identity Manager & Signer",
"version": "0.0.1",
"version": "0.0.2",
"homepage_url": "https://getgooti.com",
"options_page": "options.html",
"permissions": [
"activeTab",
"windows",
"storage"
],

View File

@@ -0,0 +1,173 @@
<!DOCTYPE html>
<html data-bs-theme="dark">
<head>
<title>Gooti - Options</title>
<link rel="stylesheet" type="text/css" href="styles.css" />
<script src="scripts.js"></script>
<style>
body {
background: var(--background);
height: 100vh;
width: 100vw;
color: #ffffff;
font-size: 16px;
box-sizing: border-box;
}
.page {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
overflow-y: auto;
}
.container {
max-width: 1200px;
box-sizing: border-box;
width: 100%;
padding-left: 1rem;
padding-right: 1rem;
}
.logo {
height: 60px;
width: 60px;
border-radius: 100%;
border: 2px solid var(--primary);
img {
height: 100%;
width: 100%;
}
}
.brand-name {
font-weight: 700;
font-size: 1.5rem;
letter-spacing: 4px;
color: #b9d6ff;
}
.main-header {
padding-top: 50px;
font-size: 50px;
font-weight: 500;
}
.sub-header {
padding-top: 28px;
font-size: 20px;
max-width: 460px;
text-align: right;
line-height: 1.4;
.accent {
color: #d63384;
border: 1px solid #d63384;
border-radius: 4px;
padding: 2px 4px;
}
}
.middle {
margin-top: 68px;
width: 100%;
background: var(--background-light);
display: flex;
flex-direction: row;
justify-content: center;
padding-bottom: 24px;
}
.option-label {
font-size: 20px;
margin-bottom: 0px;
margin-top: 16px;
}
.option-text {
color: gray;
margin-top: 4px;
}
.snapshots-list {
margin-top: 8px;
}
</style>
</head>
<body>
<div class="page">
<div class="container sam-flex-row gap" style="margin-top: 16px">
<div class="logo">
<img src="gooti.svg" alt="" />
</div>
<span class="brand-name">Gooti</span>
<span>OPTIONS</span>
</div>
<div class="container sam-flex-column center">
<span class="main-header"> Nostr Identity Manager & Signer </span>
<span class="sub-header">
Manage and switch between
<span class="accent">multiple identities</span>
while interacting with Nostr apps
</span>
</div>
<div class="middle">
<div class="container sam-flex-column">
<!-- VAULT SNAPSHOTS -->
<span class="option-label">Vault Snapshots</span>
<span class="option-text">
Importing a previously exported vault snapshot is not
<b>directly</b>
possible in the extension's popup window. This is due to the
browser's limitation of automatically closing the popup when it
looses focus, making it impossible to drop or select a file there.
</span>
<span class="option-text">
To circumvent this limitation, you need to upload your snapshot here
and make it available for the extension to import in the popup.
</span>
<span class="option-text">
<b>
Uploading a snapshot here does NOT automatically start an import!
</b>
</span>
<span class="option-text">
<b>
The data remains inside this browser and is NOT uploaded to
any server!
</b>
</span>
<div class="sam-mt-h sam-flex-row gap">
<button id="uploadSnapshotsButton" class="btn btn-primary">
Upload Snapshots
</button>
<button id="deleteSnapshotsButton" class="btn btn-danger">
Delete Snapshots
</button>
</div>
<ul id="snapshotsList" class="snapshots-list">
<!-- will be filled by JS -->
</ul>
</div>
</div>
</div>
<input
id="uploadSnapshotInput"
type="file"
multiple
style="position: absolute; top: 0; display: none"
accept=".json"
/>
<script src="options.js"></script>
</body>
</html>

View File

@@ -1,7 +1,7 @@
import { Component, inject, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { LoggerService } from '@common';
import { StartupService } from './services/startup/startup.service';
import { LoggerService, StartupService } from '@common';
import { getNewStorageServiceConfig } from './common/data/get-new-storage-service-config';
@Component({
selector: 'app-root',
@@ -15,6 +15,7 @@ export class AppComponent implements OnInit {
ngOnInit(): void {
this.#logger.initialize('Gooti Chrome Extension');
this.#startup.startOver();
this.#startup.startOver(getNewStorageServiceConfig());
}
}

View File

@@ -15,6 +15,7 @@ import { HomeComponent as EditIdentityHomeComponent } from './components/edit-id
import { KeysComponent as EditIdentityKeysComponent } from './components/edit-identity/keys/keys.component';
import { PermissionsComponent as EditIdentityPermissionsComponent } from './components/edit-identity/permissions/permissions.component';
import { RelaysComponent as EditIdentityRelaysComponent } from './components/edit-identity/relays/relays.component';
import { VaultImportComponent } from './components/vault-import/vault-import.component';
export const routes: Routes = [
{
@@ -39,6 +40,10 @@ export const routes: Routes = [
},
],
},
{
path: 'vault-import',
component: VaultImportComponent,
},
{
path: 'home',
component: HomeComponent,

View File

@@ -23,7 +23,15 @@ export class ChromeMetaHandler extends GootiMetaHandler {
await chrome.storage.local.set(data);
}
async clearData(): Promise<void> {
await chrome.storage.local.remove(this.metaProperties);
async clearData(keep: string[]): Promise<void> {
const toBeRemovedProperties: string[] = [];
for (const property of this.metaProperties) {
if (!keep.includes(property)) {
toBeRemovedProperties.push(property);
}
}
await chrome.storage.local.remove(toBeRemovedProperties);
}
}

View File

@@ -0,0 +1,16 @@
import { StorageServiceConfig } from '@common';
import { ChromeSessionHandler } from './chrome-session-handler';
import { ChromeSyncYesHandler } from './chrome-sync-yes-handler';
import { ChromeSyncNoHandler } from './chrome-sync-no-handler';
import { ChromeMetaHandler } from './chrome-meta-handler';
export const getNewStorageServiceConfig = () => {
const storageConfig: StorageServiceConfig = {
browserSessionHandler: new ChromeSessionHandler(),
browserSyncYesHandler: new ChromeSyncYesHandler(),
browserSyncNoHandler: new ChromeSyncNoHandler(),
gootiMetaHandler: new ChromeMetaHandler(),
};
return storageConfig;
};

View File

@@ -1,6 +1,10 @@
import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { IconButtonComponent, Identity_DECRYPTED, StorageService } from '@common';
import {
IconButtonComponent,
Identity_DECRYPTED,
StorageService,
} from '@common';
@Component({
selector: 'app-edit-identity',

View File

@@ -1,7 +1,11 @@
import { Component, inject, OnInit } from '@angular/core';
import { NavItemComponent } from '../../../../../../common/src/lib/components/nav-item/nav-item.component';
import { ActivatedRoute, Router } from '@angular/router';
import { ConfirmComponent, Identity_DECRYPTED, StorageService } from '@common';
import {
ConfirmComponent,
Identity_DECRYPTED,
NavItemComponent,
StorageService,
} from '@common';
@Component({
selector: 'app-home',

View File

@@ -1,12 +1,12 @@
import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import {
IconButtonComponent,
NavComponent,
NostrHelper,
StorageService,
ToastComponent,
} from '@common';
import { IconButtonComponent } from '../../../../../../common/src/lib/components/icon-button/icon-button.component';
import { FormsModule } from '@angular/forms';
interface CustomIdentity {

View File

@@ -4,12 +4,12 @@
<span>Version {{ version }}</span>
<br />
<span>&nbsp;</span>
<span> Website </span>
<a href="https://getgooti.com" target="_blank">www.getgooti.com</a>
<br />
<span>&nbsp;</span>
<span> Source code</span>
<a

View File

@@ -8,15 +8,7 @@
Export Vault
</button>
<button
class="btn btn-primary"
(click)="
confirm.show(
'Do you really want to import a vault? All existing data will be overwritten.',
onImportVault.bind(fileInput)
)
"
>
<button class="btn btn-primary" (click)="navigate('/vault-import')">
Import Vault
</button>
@@ -26,12 +18,12 @@
class="btn btn-danger"
(click)="
confirm.show(
'Do you really want to delete your vault with all identities?',
onDeleteVault.bind(this)
'Do you really want to reset your extension? Every data will be lost.',
onResetExtension.bind(this)
)
"
>
Delete Vault
Reset Extension
</button>
<lib-confirm #confirm> </lib-confirm>

View File

@@ -4,9 +4,11 @@ import {
BrowserSyncFlow,
ConfirmComponent,
DateHelper,
NavComponent,
StartupService,
StorageService,
} from '@common';
import { StartupService } from '../../../services/startup/startup.service';
import { getNewStorageServiceConfig } from '../../../common/data/get-new-storage-service-config';
@Component({
selector: 'app-settings',
@@ -14,7 +16,7 @@ import { StartupService } from '../../../services/startup/startup.service';
templateUrl: './settings.component.html',
styleUrl: './settings.component.scss',
})
export class SettingsComponent implements OnInit {
export class SettingsComponent extends NavComponent implements OnInit {
syncFlow: string | undefined;
readonly #storage = inject(StorageService);
@@ -40,10 +42,10 @@ export class SettingsComponent implements OnInit {
}
}
async onDeleteVault() {
async onResetExtension() {
try {
await this.#storage.deleteVault();
this.#startup.startOver();
await this.#storage.resetExtension();
this.#startup.startOver(getNewStorageServiceConfig());
} catch (error) {
console.log(error);
// TODO
@@ -68,7 +70,7 @@ export class SettingsComponent implements OnInit {
await this.#storage.deleteVault(true);
await this.#storage.importVault(vault);
this.#storage.isInitialized = false;
this.#startup.startOver();
this.#startup.startOver(getNewStorageServiceConfig());
} catch (error) {
console.log(error);
// TODO

View File

@@ -6,7 +6,7 @@
<div class="sam-flex-column center">
<div class="sam-flex-column gap" style="align-items: center">
<div class="logo-frame">
<img src="gooti.svg" height="120" width="120" alt=""/>
<img src="gooti.svg" height="120" width="120" alt="" />
</div>
<button
@@ -25,18 +25,10 @@
<button
type="button"
class="btn btn-secondary"
(click)="fileInput.click()"
(click)="router.navigateByUrl('/vault-import')"
>
<span>Import a vault</span>
</button>
</div>
</div>
</div>
<input
#fileInput
class="file-input"
type="file"
(change)="onImportFileChange($event)"
accept=".json"
/>

View File

@@ -1,7 +1,6 @@
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { BrowserSyncData, StorageService } from '@common';
import { StartupService } from '../../../services/startup/startup.service';
import { NavComponent } from '@common';
@Component({
selector: 'app-home',
@@ -9,28 +8,6 @@ import { StartupService } from '../../../services/startup/startup.service';
templateUrl: './home.component.html',
styleUrl: './home.component.scss',
})
export class HomeComponent {
export class HomeComponent extends NavComponent {
readonly router = inject(Router);
readonly #storage = inject(StorageService);
readonly #startup = inject(StartupService);
async onImportFileChange(event: Event) {
try {
const element = event.currentTarget as HTMLInputElement;
const file = element.files !== null ? element.files[0] : undefined;
if (!file) {
return;
}
const text = await file.text();
const vault = JSON.parse(text) as BrowserSyncData;
console.log(vault);
await this.#storage.importVault(vault);
this.#startup.startOver();
} catch (error) {
console.log(error);
// TODO
}
}
}

View File

@@ -0,0 +1,39 @@
<div class="custom-header">
<lib-icon-button
class="button"
icon="chevron-left"
(click)="navigateBack()"
></lib-icon-button>
<span class="text">Import Vault </span>
</div>
<div class="sam-pl sam-pr sam-flex-column gap">
<span class="sam-text-muted">
You can select any snapshot that you have previously uploaded on the
<a href="options.html" target="_blank">options page</a>.
</span>
<select class="form-select sam-text-sm" [(ngModel)]="selectedSnapshot">
@for(snapshot of snapshots; track snapshot) {
<option [ngValue]="snapshot">
<span>{{ snapshot.fileName }}</span>
</option>
}
</select>
<span class="sam-text-muted" style="font-size: 12px">
Please note that your data will be imported regarding your current SYNC
setting: <b>{{ syncText }}</b>
</span>
</div>
<div class="sam-flex-grow"></div>
<button
[disabled]="!selectedSnapshot"
class="import-button btn btn-primary"
(click)="onClickImport()"
>
Import
</button>

View File

@@ -0,0 +1,46 @@
:host {
height: 100%;
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
.custom-header {
padding-top: var(--size);
padding-bottom: var(--size);
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto;
align-items: center;
background: var(--background);
.button {
grid-column-start: 1;
grid-column-end: 2;
grid-row-start: 1;
grid-row-end: 2;
justify-self: start;
margin-left: 16px;
z-index: 1;
}
.text {
grid-column-start: 1;
grid-column-end: 2;
grid-row-start: 1;
grid-row-end: 2;
font-size: 20px;
font-weight: 500;
justify-self: center;
height: 32px;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 70%;
}
}
.import-button {
margin: var(--size);
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { VaultImportComponent } from './vault-import.component';
describe('VaultImportComponent', () => {
let component: VaultImportComponent;
let fixture: ComponentFixture<VaultImportComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [VaultImportComponent]
})
.compileComponents();
fixture = TestBed.createComponent(VaultImportComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,71 @@
import { Component, inject, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
BrowserSyncFlow,
GootiMetaData_VaultSnapshot,
IconButtonComponent,
NavComponent,
StartupService,
StorageService,
} from '@common';
import browser from 'webextension-polyfill';
import { getNewStorageServiceConfig } from '../../common/data/get-new-storage-service-config';
@Component({
selector: 'app-vault-import',
imports: [IconButtonComponent, FormsModule],
templateUrl: './vault-import.component.html',
styleUrl: './vault-import.component.scss',
})
export class VaultImportComponent extends NavComponent implements OnInit {
snapshots: GootiMetaData_VaultSnapshot[] = [];
selectedSnapshot: GootiMetaData_VaultSnapshot | undefined;
syncText: string | undefined;
readonly #storage = inject(StorageService);
readonly #startup = inject(StartupService);
async openOptionsPage() {
await browser.runtime.openOptionsPage();
}
ngOnInit(): void {
this.#loadData();
}
async onClickImport() {
if (!this.selectedSnapshot) {
return;
}
try {
await this.#storage.deleteVault(true);
await this.#storage.importVault(this.selectedSnapshot.data);
this.#storage.isInitialized = false;
this.#startup.startOver(getNewStorageServiceConfig());
} catch (error) {
console.log(error);
// TODO
}
}
async #loadData() {
this.snapshots = (
this.#storage.getGootiMetaHandler().gootiMetaData?.vaultSnapshots ?? []
).sortBy((x) => x.fileName, 'desc');
const syncFlow =
this.#storage.getGootiMetaHandler().gootiMetaData?.syncFlow;
switch (syncFlow) {
case BrowserSyncFlow.BROWSER_SYNC:
this.syncText = 'GOOGLE CHROME';
break;
default:
case BrowserSyncFlow.NO_SYNC:
this.syncText = 'OFF';
break;
}
}
}

View File

@@ -5,7 +5,7 @@
<div class="content-login-vault">
<div class="sam-flex-column gap" style="align-items: center">
<div class="logo-frame">
<img src="gooti.svg" height="120" width="120" alt=""/>
<img src="gooti.svg" height="120" width="120" alt="" />
</div>
<div class="sam-mt-2 input-group">
@@ -45,14 +45,14 @@
class="sam-mt"
(click)="
confirm.show(
'Do you really want to delete your vault? All existing data will be lost.',
onClickDeleteVault.bind(this)
'Do you really want to reset the extension? All data will be lost.',
onClickResetExtension.bind(this)
)
"
type="button"
class="btn btn-link"
>
Delete Vault
Reset Extension
</button>
</div>
</div>

View File

@@ -1,8 +1,8 @@
import { Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { ConfirmComponent, StorageService } from '@common';
import { StartupService } from '../../services/startup/startup.service';
import { ConfirmComponent, StartupService, StorageService } from '@common';
import { getNewStorageServiceConfig } from '../../common/data/get-new-storage-service-config';
@Component({
selector: 'app-vault-login',
@@ -43,10 +43,10 @@ export class VaultLoginComponent {
}
}
async onClickDeleteVault() {
async onClickResetExtension() {
try {
await this.#storage.deleteVault();
this.#startup.startOver();
await this.#storage.resetExtension();
this.#startup.startOver(getNewStorageServiceConfig());
} catch (error) {
console.log(error);
// TODO

View File

@@ -10,8 +10,8 @@
<span class="sam-mt sam-text-lg">Sync : Google Chrome</span>
<span class="sam-text-muted sam-text-md sam-text-align-center2">
Your encrypted data is synced between Google Chrome browser instances. You
need to be signed in with your Google account.
Your encrypted data is synced between browser instances. You need to be signed
in with your account.
</span>
<button

View File

@@ -0,0 +1,130 @@
import {
BrowserSyncData,
GOOTI_META_DATA_KEY,
GootiMetaData_VaultSnapshot,
} from '@common';
import './app/common/extensions/array';
import browser from 'webextension-polyfill';
//
// Functions
//
async function getGootiMetaDataVaultSnapshots(): Promise<
GootiMetaData_VaultSnapshot[]
> {
const data = (await browser.storage.local.get(
GOOTI_META_DATA_KEY.vaultSnapshots
)) as {
vaultSnapshots?: GootiMetaData_VaultSnapshot[];
};
return typeof data.vaultSnapshots === 'undefined'
? []
: data.vaultSnapshots.sortBy((x) => x.fileName, 'desc');
}
async function setGootiMetaDataVaultSnapshots(
vaultSnapshots: GootiMetaData_VaultSnapshot[]
): Promise<void> {
await browser.storage.local.set({
vaultSnapshots,
});
}
function rebuildSnapshotsList(snapshots: GootiMetaData_VaultSnapshot[]) {
const ul = document.getElementById('snapshotsList');
if (!ul) {
return;
}
// Clear the list
ul.innerHTML = '';
for (const snapshot of snapshots) {
const li = document.createElement('li');
const test =
'"' +
snapshot.fileName +
'"' +
' -> vault version: ' +
snapshot.data.version +
' -> identities: ' +
snapshot.data.identities.length +
' -> relays: ' +
snapshot.data.relays.length +
'';
li.innerText = test;
ul.appendChild(li);
}
}
//
// Main
//
document.addEventListener('DOMContentLoaded', async () => {
const uploadSnapshotsButton = document.getElementById(
'uploadSnapshotsButton'
);
const deleteSnapshotsButton = document.getElementById(
'deleteSnapshotsButton'
);
const uploadSnapshotInput = document.getElementById(
'uploadSnapshotInput'
) as HTMLInputElement;
deleteSnapshotsButton?.addEventListener('click', async () => {
await setGootiMetaDataVaultSnapshots([]);
rebuildSnapshotsList([]);
});
uploadSnapshotsButton?.addEventListener('click', async () => {
uploadSnapshotInput?.click();
});
uploadSnapshotInput?.addEventListener('change', async (event) => {
const files = (event.target as HTMLInputElement).files;
if (!files) {
return;
}
try {
const existingSnapshots = await getGootiMetaDataVaultSnapshots();
const newSnapshots: GootiMetaData_VaultSnapshot[] = [];
for (const file of files) {
const text = await file.text();
const vault = JSON.parse(text) as BrowserSyncData;
// Check, if the "new" file is already in the list (via fileName comparison)
if (existingSnapshots.some((x) => x.fileName === file.name)) {
continue;
}
newSnapshots.push({
fileName: file.name,
data: vault,
});
}
const snapshots = [...existingSnapshots, ...newSnapshots].sortBy(
(x) => x.fileName,
'desc'
);
// Persist the new snapshots to the local storage
await setGootiMetaDataVaultSnapshots(snapshots);
//
rebuildSnapshotsList(snapshots);
} catch (error) {
console.log(error);
}
});
const snapshots = await getGootiMetaDataVaultSnapshots();
rebuildSnapshotsList(snapshots);
});

View File

@@ -11,7 +11,8 @@
"src/background.ts",
"src/gooti-extension.ts",
"src/gooti-content-script.ts",
"src/prompt.ts"
"src/prompt.ts",
"src/options.ts"
],
"include": ["src/**/*.d.ts"]
}

View File

@@ -1,5 +1,14 @@
import { inject } from '@angular/core';
import { Router } from '@angular/router';
export class NavComponent {
readonly #router = inject(Router);
navigateBack() {
window.history.back();
}
navigate(path: string) {
this.#router.navigate([path]);
}
}

View File

@@ -1,10 +1,10 @@
import { inject, Injectable } from '@angular/core';
import { LoggerService, StorageService, StorageServiceConfig } from '@common';
import { ChromeSessionHandler } from '../../common/data/chrome-session-handler';
import { ChromeSyncYesHandler } from '../../common/data/chrome-sync-yes-handler';
import { ChromeSyncNoHandler } from '../../common/data/chrome-sync-no-handler';
import { ChromeMetaHandler } from '../../common/data/chrome-meta-handler';
import { Router } from '@angular/router';
import { LoggerService } from '../logger/logger.service';
import {
StorageService,
StorageServiceConfig,
} from '../storage/storage.service';
@Injectable({
providedIn: 'root',
@@ -14,14 +14,7 @@ export class StartupService {
readonly #storage = inject(StorageService);
readonly #router = inject(Router);
async startOver() {
const storageConfig: StorageServiceConfig = {
browserSessionHandler: new ChromeSessionHandler(),
browserSyncYesHandler: new ChromeSyncYesHandler(),
browserSyncNoHandler: new ChromeSyncNoHandler(),
gootiMetaHandler: new ChromeMetaHandler(),
};
async startOver(storageConfig: StorageServiceConfig) {
this.#storage.initialize(storageConfig);
// Step 0:

View File

@@ -8,7 +8,7 @@ export abstract class GootiMetaHandler {
#gootiMetaData?: GootiMetaData;
readonly metaProperties = ['syncFlow'];
readonly metaProperties = ['syncFlow', 'vaultSnapshots'];
/**
* Load the full data from the storage. If the storage is used for storing
* other data (e.g. browser sync data when the user decided to NOT sync),
@@ -39,5 +39,5 @@ export abstract class GootiMetaHandler {
await this.saveFullData(this.#gootiMetaData);
}
abstract clearData(): Promise<void>;
abstract clearData(keep: string[]): Promise<void>;
}

View File

@@ -120,7 +120,6 @@ export const deleteVault = async function (
await this.getBrowserSyncHandler().clearData();
await this.getBrowserSessionHandler().clearData();
await this.getGootiMetaHandler().clearData();
if (!doNotSetIsInitializedToFalse) {
this.isInitialized = false;

View File

@@ -115,6 +115,14 @@ export class StorageService {
await deleteVault.call(this, doNotSetIsInitializedToFalse);
}
async resetExtension() {
this.assureIsInitialized();
await this.getBrowserSyncHandler().clearData();
await this.getBrowserSessionHandler().clearData();
await this.getGootiMetaHandler().clearData([]);
this.isInitialized = false;
}
async unlockVault(password: string): Promise<void> {
await unlockVault.call(this, password);
}

View File

@@ -79,6 +79,17 @@ export interface BrowserSessionData {
relays: Relay_DECRYPTED[];
}
export interface GootiMetaData_VaultSnapshot {
fileName: string;
data: BrowserSyncData;
}
export const GOOTI_META_DATA_KEY = {
vaultSnapshots: 'vaultSnapshots',
};
export interface GootiMetaData {
syncFlow?: number; // 0 = no sync, 1 = browser sync, (future: 2 = Gooti sync, 3 = Custom sync (bring your own sync))
vaultSnapshots?: GootiMetaData_VaultSnapshot[];
}

View File

@@ -10,6 +10,10 @@
margin-top: var(--size-h);
}
.sam-mt-hh {
margin-top: var(--size-hh);
}
.sam-mb {
margin-bottom: var(--size);
}

View File

@@ -8,6 +8,7 @@
--size-2: 32px;
--size: 16px;
--size-h: 8px;
--size-hh: 4px;
--background: #161c26;
--background-light: #202733;

View File

@@ -21,6 +21,7 @@ export * from './lib/services/storage/browser-sync-handler';
export * from './lib/services/storage/browser-session-handler';
export * from './lib/services/storage/gooti-meta-handler';
export * from './lib/services/logger/logger.service';
export * from './lib/services/startup/startup.service';
// Components
export * from './lib/components/icon-button/icon-button.component';

View File

@@ -0,0 +1,26 @@
import type { Configuration } from 'webpack';
module.exports = {
entry: {
background: {
import: 'src/background.ts',
runtime: false,
},
'gooti-extension': {
import: 'src/gooti-extension.ts',
runtime: false,
},
'gooti-content-script': {
import: 'src/gooti-content-script.ts',
runtime: false,
},
prompt: {
import: 'src/prompt.ts',
runtime: false,
},
options: {
import: 'src/options.ts',
runtime: false,
},
},
} as Configuration;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Origami-Paper-Bird--Streamline-Cyber.svg" height="24" width="24"><desc>Origami Paper Bird Streamline Icon: https://streamlinehq.com</desc><path fill="#ffffff" d="M4.66378 6.62012 0.751282 10.9774H6.11588l0.5042 7.3802 11.73752 4.8911L9.55488 9.01202l-4.8911 -2.3919Z" stroke-width="1"></path><path fill="#ffffff" d="M18.8423 0.751343 9.55499 9.01194l5.32571 8.61246L18.8423 0.751343Z" stroke-width="1"></path><path fill="#bbd8ff" d="m9.555 9.01187 -1.4675 -0.7178 -1.7681 5.66933 0.3007 4.3942L9.555 9.01187Z" stroke-width="1"></path><path fill="#bbd8ff" d="m4.66378 6.62012 1.4521 4.35728H0.751282L4.66378 6.62012Z" stroke-width="1"></path><path fill="#bbd8ff" d="m15.3767 18.4282 7.872 -15.23167 -5.5814 2.5565 -2.7866 11.87137 0.496 0.8038Z" stroke-width="1"></path><path stroke="#092f63" stroke-linejoin="round" stroke-miterlimit="10" d="m9.55488 9.01202 -4.8911 -2.3919L0.751282 10.9774H6.11588l0.5042 7.3802 11.73752 4.8911L9.55488 9.01202Z" stroke-width="1"></path><path stroke="#092f63" stroke-linejoin="round" stroke-miterlimit="10" d="M9.55499 9.01194 18.8423 0.751343 14.8807 17.6244" stroke-width="1"></path><path stroke="#092f63" stroke-linejoin="round" stroke-miterlimit="10" d="m17.6673 5.75303 5.5814 -2.5565 -7.872 15.23167" stroke-width="1"></path><path stroke="#092f63" stroke-linejoin="round" stroke-miterlimit="10" d="m4.66382 6.62012 1.4521 4.35728" stroke-width="1"></path><path stroke="#092f63" stroke-linejoin="round" stroke-miterlimit="10" d="m6.62109 18.3564 2.9338 -9.34456" stroke-width="1"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Origami-Paper-Bird--Streamline-Cyber.svg" height="24" width="24"><desc>Origami Paper Bird Streamline Icon: https://streamlinehq.com</desc><path fill="#ffffff" d="M4.66378 6.62012 0.751282 10.9774H6.11588l0.5042 7.3802 11.73752 4.8911L9.55488 9.01202l-4.8911 -2.3919Z" stroke-width="1"></path><path fill="#ffffff" d="M18.8423 0.751343 9.55499 9.01194l5.32571 8.61246L18.8423 0.751343Z" stroke-width="1"></path><path fill="#bbd8ff" d="m9.555 9.01187 -1.4675 -0.7178 -1.7681 5.66933 0.3007 4.3942L9.555 9.01187Z" stroke-width="1"></path><path fill="#bbd8ff" d="m4.66378 6.62012 1.4521 4.35728H0.751282L4.66378 6.62012Z" stroke-width="1"></path><path fill="#bbd8ff" d="m15.3767 18.4282 7.872 -15.23167 -5.5814 2.5565 -2.7866 11.87137 0.496 0.8038Z" stroke-width="1"></path><path stroke="#0d6efd" stroke-linejoin="round" stroke-miterlimit="10" d="m9.55488 9.01202 -4.8911 -2.3919L0.751282 10.9774H6.11588l0.5042 7.3802 11.73752 4.8911L9.55488 9.01202Z" stroke-width="1"></path><path stroke="#0d6efd" stroke-linejoin="round" stroke-miterlimit="10" d="M9.55499 9.01194 18.8423 0.751343 14.8807 17.6244" stroke-width="1"></path><path stroke="#0d6efd" stroke-linejoin="round" stroke-miterlimit="10" d="m17.6673 5.75303 5.5814 -2.5565 -7.872 15.23167" stroke-width="1"></path><path stroke="#0d6efd" stroke-linejoin="round" stroke-miterlimit="10" d="m4.66382 6.62012 1.4521 4.35728" stroke-width="1"></path><path stroke="#0d6efd" stroke-linejoin="round" stroke-miterlimit="10" d="m6.62109 18.3564 2.9338 -9.34456" stroke-width="1"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,47 @@
{
"manifest_version": 3,
"name": "Gooti",
"description": "Nostr Identity Manager & Signer",
"version": "0.0.3",
"homepage_url": "https://getgooti.com",
"options_page": "options.html",
"permissions": [
"storage"
],
"action": {
"default_popup": "index.html",
"default_icon": "gooti-with-bg.png"
},
"background": {
"scripts": [
"background.js"
]
},
"content_scripts": [
{
"run_at": "document_start",
"matches": [
"<all_urls>"
],
"js": [
"gooti-content-script.js"
],
"all_frames": true
}
],
"web_accessible_resources": [
{
"resources": [
"gooti-extension.js"
],
"matches": [
"<all_urls>"
]
}
],
"browser_specific_settings": {
"gecko": {
"id": "firefox@getgooti.com"
}
}
}

View File

@@ -0,0 +1,173 @@
<!DOCTYPE html>
<html data-bs-theme="dark">
<head>
<title>Gooti - Options</title>
<link rel="stylesheet" type="text/css" href="styles.css" />
<script src="scripts.js"></script>
<style>
body {
background: var(--background);
height: 100vh;
width: 100vw;
color: #ffffff;
font-size: 16px;
box-sizing: border-box;
}
.page {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
overflow-y: auto;
}
.container {
max-width: 1200px;
box-sizing: border-box;
width: 100%;
padding-left: 1rem;
padding-right: 1rem;
}
.logo {
height: 60px;
width: 60px;
border-radius: 100%;
border: 2px solid var(--primary);
img {
height: 100%;
width: 100%;
}
}
.brand-name {
font-weight: 700;
font-size: 1.5rem;
letter-spacing: 4px;
color: #b9d6ff;
}
.main-header {
padding-top: 50px;
font-size: 50px;
font-weight: 500;
}
.sub-header {
padding-top: 28px;
font-size: 20px;
max-width: 460px;
text-align: right;
line-height: 1.4;
.accent {
color: #d63384;
border: 1px solid #d63384;
border-radius: 4px;
padding: 2px 4px;
}
}
.middle {
margin-top: 68px;
width: 100%;
background: var(--background-light);
display: flex;
flex-direction: row;
justify-content: center;
padding-bottom: 24px;
}
.option-label {
font-size: 20px;
margin-bottom: 0px;
margin-top: 16px;
}
.option-text {
color: gray;
margin-top: 4px;
}
.snapshots-list {
margin-top: 8px;
}
</style>
</head>
<body>
<div class="page">
<div class="container sam-flex-row gap" style="margin-top: 16px">
<div class="logo">
<img src="gooti.svg" alt="" />
</div>
<span class="brand-name">Gooti</span>
<span>OPTIONS</span>
</div>
<div class="container sam-flex-column center">
<span class="main-header"> Nostr Identity Manager & Signer </span>
<span class="sub-header">
Manage and switch between
<span class="accent">multiple identities</span>
while interacting with Nostr apps
</span>
</div>
<div class="middle">
<div class="container sam-flex-column">
<!-- VAULT SNAPSHOTS -->
<span class="option-label">Vault Snapshots</span>
<span class="option-text">
Importing a previously exported vault snapshot is not
<b>directly</b>
possible in the extension's popup window. This is due to the
browser's limitation of automatically closing the popup when it
looses focus, making it impossible to drop or select a file there.
</span>
<span class="option-text">
To circumvent this limitation, you need to upload your snapshot here
and make it available for the extension to import in the popup.
</span>
<span class="option-text">
<b>
Uploading a snapshot here does NOT automatically start an import!
</b>
</span>
<span class="option-text">
<b>
The data remains inside this browser and is NOT uploaded to any
server!
</b>
</span>
<div class="sam-mt-h sam-flex-row gap">
<button id="uploadSnapshotsButton" class="btn btn-primary">
Upload Snapshots
</button>
<button id="deleteSnapshotsButton" class="btn btn-danger">
Delete Snapshots
</button>
</div>
<ul id="snapshotsList" class="snapshots-list">
<!-- will be filled by JS -->
</ul>
</div>
</div>
</div>
<input
id="uploadSnapshotInput"
type="file"
multiple
style="position: absolute; top: 0; display: none"
accept=".json"
/>
<script src="options.js"></script>
</body>
</html>

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="#ffffff" class="bi bi-person-fill" viewBox="0 0 16 16">
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6"/>
</svg>

After

Width:  |  Height:  |  Size: 219 B

View File

@@ -0,0 +1,221 @@
<!DOCTYPE html>
<html data-bs-theme="dark">
<head>
<title>Gooti</title>
<link rel="stylesheet" type="text/css" href="styles.css" />
<script src="scripts.js"></script>
<style>
body {
background: var(--background);
height: 100vh;
width: 100vw;
color: #ffffff;
font-size: 16px;
}
.color-primary {
color: var(--primary);
}
.page {
height: 100%;
display: grid;
grid-template-rows: 1fr 60px;
grid-template-columns: 1fr;
overflow-y: hidden;
}
.card {
padding: var(--size);
background: var(--background-light);
border-radius: 8px;
color: #ffffff;
display: flex;
flex-direction: column;
}
.json {
white-space: pre;
overflow-y: auto;
font-size: 12px;
color: gray;
}
.text {
white-space: normal;
overflow-y: auto;
font-size: 12px;
color: gray;
}
</style>
</head>
<body>
<div class="page">
<div class="sam-flex-column" style="overflow-y: auto">
<div class="sam-text-header">
<span id="titleSpan" style="font-weight: 400 !important"></span>
</div>
<span
class="host-INSERT sam-align-self-center sam-text-muted"
style="font-weight: 500"
></span>
<!-- Card for getPublicKey -->
<div id="cardGetPublicKey" class="card sam-mt sam-ml sam-mr">
<span style="text-align: center">
<b><span class="host-INSERT color-primary"></span></b>
is requesting permission to<br />
<br />
<b class="color-primary">read your public key</b> <br />
<br />
<span>
for the selected identity
<span
style="font-weight: 500"
class="nick-INSERT color-primary"
></span>
</span>
</span>
</div>
<!-- Card for getRelays -->
<div id="cardGetRelays" class="card sam-mt sam-ml sam-mr">
<span style="text-align: center">
<b><span class="host-INSERT color-primary"></span></b>
is requesting permission to<br />
<br />
<b class="color-primary">read your relays</b> <br />
<br />
<span>
for the selected identity
<span
style="font-weight: 500"
class="nick-INSERT color-primary"
></span>
</span>
</span>
</div>
<!-- Card for signEvent -->
<div id="cardSignEvent" class="card sam-mt sam-ml sam-mr">
<span style="text-align: center">
<b><span class="host-INSERT color-primary"></span></b>
is requesting permission to<br />
<br />
<b class="color-primary">sign an event</b> (kind
<span id="kindSpan"></span>) <br />
<br />
<span>
for the selected identity
<span
style="font-weight: 500"
class="nick-INSERT color-primary"
></span>
</span>
</span>
</div>
<!-- Card2 for signEvent -->
<div id="card2SignEvent" class="card sam-mt sam-ml sam-mr">
<div id="card2SignEvent_json" class="json"></div>
</div>
<!-- Card for nip04.encrypt -->
<div id="cardNip04Encrypt" class="card sam-mt sam-ml sam-mr">
<span style="text-align: center">
<b><span class="host-INSERT color-primary"></span></b>
is requesting permission to<br />
<br />
<b class="color-primary">encrypt a text</b> (NIP04) <br />
<br />
<span>
for the selected identity
<span
style="font-weight: 500"
class="nick-INSERT color-primary"
></span>
</span>
</span>
</div>
<!-- Card2 for nip04.encrypt -->
<div id="card2Nip04Encrypt" class="card sam-mt sam-ml sam-mr">
<div id="card2Nip04Encrypt_text" class="text"></div>
</div>
<!-- Card for nip04.decrypt -->
<div id="cardNip04Decrypt" class="card sam-mt sam-ml sam-mr">
<span style="text-align: center">
<b><span class="host-INSERT color-primary"></span></b>
is requesting permission to<br />
<br />
<b class="color-primary">decrypt a text</b> (NIP04) <br />
<br />
<span>
for the selected identity
<span
style="font-weight: 500"
class="nick-INSERT color-primary"
></span>
</span>
</span>
</div>
<!-- Card2 for nip04.decrypt -->
<div id="card2Nip04Decrypt" class="card sam-mt sam-ml sam-mr">
<div id="card2Nip04Decrypt_text" class="text"></div>
</div>
</div>
<!------------->
<!-- ACTIONS -->
<!------------->
<div class="sam-footer-grid-2">
<div class="btn-group">
<button id="rejectButton" type="button" class="btn btn-secondary">
Reject
</button>
<button
type="button"
class="btn btn-secondary dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button id="rejectJustOnceButton" class="dropdown-item">
just once
</button>
</li>
</ul>
</div>
<div class="btn-group">
<button id="approveButton" type="button" class="btn btn-primary">
Approve
</button>
<button
type="button"
class="btn btn-primary dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li>
<button id="approveJustOnceButton" class="dropdown-item" href="#">
just once
</button>
</li>
</ul>
</div>
</div>
</div>
<script src="prompt.js"></script>
</body>
</html>

View File

@@ -1,336 +1 @@
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * Delete the template below * * * * * * * * * -->
<!-- * * * * * * * to get started with your project! * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<style>
:host {
--bright-blue: oklch(51.01% 0.274 263.83);
--electric-violet: oklch(53.18% 0.28 296.97);
--french-violet: oklch(47.66% 0.246 305.88);
--vivid-pink: oklch(69.02% 0.277 332.77);
--hot-red: oklch(61.42% 0.238 15.34);
--orange-red: oklch(63.32% 0.24 31.68);
--gray-900: oklch(19.37% 0.006 300.98);
--gray-700: oklch(36.98% 0.014 302.71);
--gray-400: oklch(70.9% 0.015 304.04);
--red-to-pink-to-purple-vertical-gradient: linear-gradient(
180deg,
var(--orange-red) 0%,
var(--vivid-pink) 50%,
var(--electric-violet) 100%
);
--red-to-pink-to-purple-horizontal-gradient: linear-gradient(
90deg,
var(--orange-red) 0%,
var(--vivid-pink) 50%,
var(--electric-violet) 100%
);
--pill-accent: var(--bright-blue);
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol";
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1 {
font-size: 3.125rem;
color: var(--gray-900);
font-weight: 500;
line-height: 100%;
letter-spacing: -0.125rem;
margin: 0;
font-family: "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol";
}
p {
margin: 0;
color: var(--gray-700);
}
main {
width: 100%;
min-height: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
box-sizing: inherit;
position: relative;
}
.angular-logo {
max-width: 9.2rem;
}
.content {
display: flex;
justify-content: space-around;
width: 100%;
max-width: 700px;
margin-bottom: 3rem;
}
.content h1 {
margin-top: 1.75rem;
}
.content p {
margin-top: 1.5rem;
}
.divider {
width: 1px;
background: var(--red-to-pink-to-purple-vertical-gradient);
margin-inline: 0.5rem;
}
.pill-group {
display: flex;
flex-direction: column;
align-items: start;
flex-wrap: wrap;
gap: 1.25rem;
}
.pill {
display: flex;
align-items: center;
--pill-accent: var(--bright-blue);
background: color-mix(in srgb, var(--pill-accent) 5%, transparent);
color: var(--pill-accent);
padding-inline: 0.75rem;
padding-block: 0.375rem;
border-radius: 2.75rem;
border: 0;
transition: background 0.3s ease;
font-family: var(--inter-font);
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: 1.4rem;
letter-spacing: -0.00875rem;
text-decoration: none;
}
.pill:hover {
background: color-mix(in srgb, var(--pill-accent) 15%, transparent);
}
.pill-group .pill:nth-child(6n + 1) {
--pill-accent: var(--bright-blue);
}
.pill-group .pill:nth-child(6n + 2) {
--pill-accent: var(--french-violet);
}
.pill-group .pill:nth-child(6n + 3),
.pill-group .pill:nth-child(6n + 4),
.pill-group .pill:nth-child(6n + 5) {
--pill-accent: var(--hot-red);
}
.pill-group svg {
margin-inline-start: 0.25rem;
}
.social-links {
display: flex;
align-items: center;
gap: 0.73rem;
margin-top: 1.5rem;
}
.social-links path {
transition: fill 0.3s ease;
fill: var(--gray-400);
}
.social-links a:hover svg path {
fill: var(--gray-900);
}
@media screen and (max-width: 650px) {
.content {
flex-direction: column;
width: max-content;
}
.divider {
height: 1px;
width: 100%;
background: var(--red-to-pink-to-purple-horizontal-gradient);
margin-block: 1.5rem;
}
}
</style>
<main class="main">
<div class="content">
<div class="left-side">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 982 239"
fill="none"
class="angular-logo"
>
<g clip-path="url(#a)">
<path
fill="url(#b)"
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
/>
<path
fill="url(#c)"
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
/>
</g>
<defs>
<radialGradient
id="c"
cx="0"
cy="0"
r="1"
gradientTransform="rotate(118.122 171.182 60.81) scale(205.794)"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#FF41F8" />
<stop offset=".707" stop-color="#FF41F8" stop-opacity=".5" />
<stop offset="1" stop-color="#FF41F8" stop-opacity="0" />
</radialGradient>
<linearGradient
id="b"
x1="0"
x2="982"
y1="192"
y2="192"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#F0060B" />
<stop offset="0" stop-color="#F0070C" />
<stop offset=".526" stop-color="#CC26D5" />
<stop offset="1" stop-color="#7702FF" />
</linearGradient>
<clipPath id="a"><path fill="#fff" d="M0 0h982v239H0z" /></clipPath>
</defs>
</svg>
<h1>Hello, {{ title }}</h1>
<p>Congratulations! Your app is running. 🎉</p>
</div>
<div class="divider" role="separator" aria-label="Divider"></div>
<div class="right-side">
<div class="pill-group">
@for (item of [
{ title: 'Explore the Docs', link: 'https://angular.dev' },
{ title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' },
{ title: 'CLI Docs', link: 'https://angular.dev/tools/cli' },
{ title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' },
{ title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' },
]; track item.title) {
<a
class="pill"
[href]="item.link"
target="_blank"
rel="noopener"
>
<span>{{ item.title }}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
height="14"
viewBox="0 -960 960 960"
width="14"
fill="currentColor"
>
<path
d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"
/>
</svg>
</a>
}
</div>
<div class="social-links">
<a
href="https://github.com/angular/angular"
aria-label="Github"
target="_blank"
rel="noopener"
>
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
alt="Github"
>
<path
d="M12.3047 0C5.50634 0 0 5.50942 0 12.3047C0 17.7423 3.52529 22.3535 8.41332 23.9787C9.02856 24.0946 9.25414 23.7142 9.25414 23.3871C9.25414 23.0949 9.24389 22.3207 9.23876 21.2953C5.81601 22.0377 5.09414 19.6444 5.09414 19.6444C4.53427 18.2243 3.72524 17.8449 3.72524 17.8449C2.61064 17.082 3.81137 17.0973 3.81137 17.0973C5.04697 17.1835 5.69604 18.3647 5.69604 18.3647C6.79321 20.2463 8.57636 19.7029 9.27978 19.3881C9.39052 18.5924 9.70736 18.0499 10.0591 17.7423C7.32641 17.4347 4.45429 16.3765 4.45429 11.6618C4.45429 10.3185 4.9311 9.22133 5.72065 8.36C5.58222 8.04931 5.16694 6.79833 5.82831 5.10337C5.82831 5.10337 6.85883 4.77319 9.2121 6.36459C10.1965 6.09082 11.2424 5.95546 12.2883 5.94931C13.3342 5.95546 14.3801 6.09082 15.3644 6.36459C17.7023 4.77319 18.7328 5.10337 18.7328 5.10337C19.3942 6.79833 18.9789 8.04931 18.8559 8.36C19.6403 9.22133 20.1171 10.3185 20.1171 11.6618C20.1171 16.3888 17.2409 17.4296 14.5031 17.7321C14.9338 18.1012 15.3337 18.8559 15.3337 20.0084C15.3337 21.6552 15.3183 22.978 15.3183 23.3779C15.3183 23.7009 15.5336 24.0854 16.1642 23.9623C21.0871 22.3484 24.6094 17.7341 24.6094 12.3047C24.6094 5.50942 19.0999 0 12.3047 0Z"
/>
</svg>
</a>
<a
href="https://twitter.com/angular"
aria-label="Twitter"
target="_blank"
rel="noopener"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
alt="Twitter"
>
<path
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
/>
</svg>
</a>
<a
href="https://www.youtube.com/channel/UCbn1OgGei-DV7aSRo_HaAiw"
aria-label="Youtube"
target="_blank"
rel="noopener"
>
<svg
width="29"
height="20"
viewBox="0 0 29 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
alt="Youtube"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M27.4896 1.52422C27.9301 1.96749 28.2463 2.51866 28.4068 3.12258C29.0004 5.35161 29.0004 10 29.0004 10C29.0004 10 29.0004 14.6484 28.4068 16.8774C28.2463 17.4813 27.9301 18.0325 27.4896 18.4758C27.0492 18.9191 26.5 19.2389 25.8972 19.4032C23.6778 20 14.8068 20 14.8068 20C14.8068 20 5.93586 20 3.71651 19.4032C3.11363 19.2389 2.56449 18.9191 2.12405 18.4758C1.68361 18.0325 1.36732 17.4813 1.20683 16.8774C0.613281 14.6484 0.613281 10 0.613281 10C0.613281 10 0.613281 5.35161 1.20683 3.12258C1.36732 2.51866 1.68361 1.96749 2.12405 1.52422C2.56449 1.08095 3.11363 0.76113 3.71651 0.596774C5.93586 0 14.8068 0 14.8068 0C14.8068 0 23.6778 0 25.8972 0.596774C26.5 0.76113 27.0492 1.08095 27.4896 1.52422ZM19.3229 10L11.9036 5.77905V14.221L19.3229 10Z"
/>
</svg>
</a>
</div>
</div>
</div>
</main>
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * The content above * * * * * * * * * * * * -->
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * * -->
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * End of Placeholder * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<router-outlet />
<router-outlet></router-outlet>

View File

@@ -1,12 +1,21 @@
import { Component } from '@angular/core';
import { Component, inject, OnInit } from '@angular/core';
import { LoggerService, StartupService } from '@common';
import { getNewStorageServiceConfig } from './common/data/get-new-storage-service-config';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
styleUrl: './app.component.scss',
})
export class AppComponent {
title = 'firefox';
export class AppComponent implements OnInit {
readonly #startup = inject(StartupService);
readonly #logger = inject(LoggerService);
ngOnInit(): void {
this.#logger.initialize('Gooti Firefox Extension');
this.#startup.startOver(getNewStorageServiceConfig());
}
}

View File

@@ -1,3 +1,95 @@
import { Routes } from '@angular/router';
import { HomeComponent as VaultCreateHomeComponent } from './components/vault-create/home/home.component';
import { NewComponent as VaultCreateNewComponent } from './components/vault-create/new/new.component';
import { HomeComponent } from './components/home/home.component';
import { IdentitiesComponent } from './components/home/identities/identities.component';
import { IdentityComponent } from './components/home/identity/identity.component';
import { InfoComponent } from './components/home/info/info.component';
import { SettingsComponent } from './components/home/settings/settings.component';
import { NewIdentityComponent } from './components/new-identity/new-identity.component';
import { EditIdentityComponent } from './components/edit-identity/edit-identity.component';
import { HomeComponent as EditIdentityHomeComponent } from './components/edit-identity/home/home.component';
import { KeysComponent as EditIdentityKeysComponent } from './components/edit-identity/keys/keys.component';
import { PermissionsComponent as EditIdentityPermissionsComponent } from './components/edit-identity/permissions/permissions.component';
import { RelaysComponent as EditIdentityRelaysComponent } from './components/edit-identity/relays/relays.component';
import { WelcomeComponent } from './components/welcome/welcome.component';
import { VaultLoginComponent } from './components/vault-login/vault-login.component';
import { VaultCreateComponent } from './components/vault-create/vault-create.component';
import { VaultImportComponent } from './components/vault-import/vault-import.component';
export const routes: Routes = [];
export const routes: Routes = [
{
path: 'welcome',
component: WelcomeComponent,
},
{
path: 'vault-login',
component: VaultLoginComponent,
},
{
path: 'vault-create',
component: VaultCreateComponent,
children: [
{
path: 'home',
component: VaultCreateHomeComponent,
},
{
path: 'new',
component: VaultCreateNewComponent,
},
],
},
{
path: 'vault-import',
component: VaultImportComponent,
},
{
path: 'home',
component: HomeComponent,
children: [
{
path: 'identities',
component: IdentitiesComponent,
},
{
path: 'identity',
component: IdentityComponent,
},
{
path: 'info',
component: InfoComponent,
},
{
path: 'settings',
component: SettingsComponent,
},
],
},
{
path: 'new-identity',
component: NewIdentityComponent,
},
{
path: 'edit-identity/:id',
component: EditIdentityComponent,
children: [
{
path: 'home',
component: EditIdentityHomeComponent,
},
{
path: 'keys',
component: EditIdentityKeysComponent,
},
{
path: 'permissions',
component: EditIdentityPermissionsComponent,
},
{
path: 'relays',
component: EditIdentityRelaysComponent,
},
],
},
];

View File

@@ -0,0 +1,39 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { GootiMetaData, GootiMetaHandler } from '@common';
import browser from 'webextension-polyfill';
export class FirefoxMetaHandler extends GootiMetaHandler {
async loadFullData(): Promise<Partial<Record<string, any>>> {
const dataWithPossibleAlienProperties = await browser.storage.local.get(
null
);
if (Object.keys(dataWithPossibleAlienProperties).length === 0) {
return dataWithPossibleAlienProperties;
}
const data: Partial<Record<string, any>> = {};
this.metaProperties.forEach((property) => {
data[property] = dataWithPossibleAlienProperties[property];
});
return data;
}
async saveFullData(data: GootiMetaData): Promise<void> {
await browser.storage.local.set(data as Record<string, any>);
console.log(data);
}
async clearData(keep: string[]): Promise<void> {
const toBeRemovedProperties: string[] = [];
for (const property of this.metaProperties) {
if (!keep.includes(property)) {
toBeRemovedProperties.push(property);
}
}
await browser.storage.local.remove(this.metaProperties);
}
}

View File

@@ -0,0 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { BrowserSessionData, BrowserSessionHandler } from '@common';
import browser from 'webextension-polyfill';
export class FirefoxSessionHandler extends BrowserSessionHandler {
async loadFullData(): Promise<Partial<Record<string, any>>> {
return browser.storage.session.get(null);
}
async saveFullData(data: BrowserSessionData): Promise<void> {
await browser.storage.session.set(data as Record<string, any>);
}
async clearData(): Promise<void> {
await browser.storage.session.clear();
}
}

View File

@@ -0,0 +1,63 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
BrowserSyncData,
Identity_ENCRYPTED,
Permission_ENCRYPTED,
BrowserSyncHandler,
Relay_ENCRYPTED,
} from '@common';
import browser from 'webextension-polyfill';
/**
* Handles the browser sync operations when the browser sync is enabled.
* If it's not enabled, it behaves like the local extension storage (which is fine).
*/
export class FirefoxSyncNoHandler extends BrowserSyncHandler {
async loadUnmigratedData(): Promise<Partial<Record<string, any>>> {
const data = await browser.storage.local.get(null);
// Remove any available "ignore properties".
this.ignoreProperties.forEach((property) => {
delete data[property];
});
return data;
}
async saveAndSetFullData(data: BrowserSyncData): Promise<void> {
await browser.storage.local.set(data as Record<string, any>);
this.setFullData(data);
}
async saveAndSetPartialData_Permissions(data: {
permissions: Permission_ENCRYPTED[];
}): Promise<void> {
await browser.storage.local.set(data);
this.setPartialData_Permissions(data);
}
async saveAndSetPartialData_Identities(data: {
identities: Identity_ENCRYPTED[];
}): Promise<void> {
await browser.storage.local.set(data);
this.setPartialData_Identities(data);
}
async saveAndSetPartialData_SelectedIdentityId(data: {
selectedIdentityId: string | null;
}): Promise<void> {
await browser.storage.local.set(data);
this.setPartialData_SelectedIdentityId(data);
}
async saveAndSetPartialData_Relays(data: {
relays: Relay_ENCRYPTED[];
}): Promise<void> {
await browser.storage.local.set(data);
this.setPartialData_Relays(data);
}
async clearData(): Promise<void> {
const props = Object.keys(await this.loadUnmigratedData());
await browser.storage.local.remove(props);
}
}

View File

@@ -0,0 +1,56 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
BrowserSyncData,
Identity_ENCRYPTED,
Permission_ENCRYPTED,
BrowserSyncHandler,
Relay_ENCRYPTED,
} from '@common';
import browser from 'webextension-polyfill';
/**
* Handles the browser sync operations when the browser sync is enabled.
* If it's not enabled, it behaves like the local extension storage (which is fine).
*/
export class FirefoxSyncYesHandler extends BrowserSyncHandler {
async loadUnmigratedData(): Promise<Partial<Record<string, any>>> {
return await browser.storage.sync.get(null);
}
async saveAndSetFullData(data: BrowserSyncData): Promise<void> {
await browser.storage.sync.set(data as Record<string, any>);
this.setFullData(data);
}
async saveAndSetPartialData_Permissions(data: {
permissions: Permission_ENCRYPTED[];
}): Promise<void> {
await browser.storage.sync.set(data);
this.setPartialData_Permissions(data);
}
async saveAndSetPartialData_Identities(data: {
identities: Identity_ENCRYPTED[];
}): Promise<void> {
await browser.storage.sync.set(data);
this.setPartialData_Identities(data);
}
async saveAndSetPartialData_SelectedIdentityId(data: {
selectedIdentityId: string | null;
}): Promise<void> {
await browser.storage.sync.set(data);
this.setPartialData_SelectedIdentityId(data);
}
async saveAndSetPartialData_Relays(data: {
relays: Relay_ENCRYPTED[];
}): Promise<void> {
await browser.storage.sync.set(data);
this.setPartialData_Relays(data);
}
async clearData(): Promise<void> {
await browser.storage.sync.clear();
}
}

View File

@@ -0,0 +1,15 @@
import { FirefoxMetaHandler } from './firefox-meta-handler';
import { FirefoxSessionHandler } from './firefox-session-handler';
import { FirefoxSyncNoHandler } from './firefox-sync-no-handler';
import { FirefoxSyncYesHandler } from './firefox-sync-yes-handler';
export const getNewStorageServiceConfig = () => {
const storageConfig = {
browserSessionHandler: new FirefoxSessionHandler(),
browserSyncYesHandler: new FirefoxSyncYesHandler(),
browserSyncNoHandler: new FirefoxSyncNoHandler(),
gootiMetaHandler: new FirefoxMetaHandler(),
};
return storageConfig;
};

View File

@@ -0,0 +1,95 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
declare global {
interface Array<T> {
/**
* Sorts the array by the provided property and returns a new sorted array.
* Default sorting is ASC. You can apply DESC sorting by using the optional parameter "order = 'desc'"
*/
sortBy<K>(keyFunction: (t: T) => K, order?: 'asc' | 'desc'): T[];
/** Check if the array is empty. */
empty(): boolean;
groupBy<K, R>(
keyFunction: (t: T) => K,
reduceFn: (items: T[]) => R
): Map<K, R>;
}
}
if (!Array.prototype.empty) {
Array.prototype.empty = function (): boolean {
return this.length === 0;
};
}
if (!Array.prototype.sortBy) {
Array.prototype.sortBy = function <T, K>(
keyFunction: (t: T) => K,
order?: string
): T[] {
if (this.length === 0) {
return [];
}
// determine sort order (asc or desc / asc is default)
let asc = true;
if (order === 'desc') {
asc = false;
}
const arrayClone = Array.from(this) as any[];
const firstSortProperty = keyFunction(arrayClone[0]);
if (typeof firstSortProperty === 'string') {
// string in-place sort
arrayClone.sort((a, b) => {
if (asc) {
return ('' + (keyFunction(a) as unknown as string)).localeCompare(
keyFunction(b) as unknown as string
);
}
return ('' + (keyFunction(b) as unknown as string)).localeCompare(
keyFunction(a) as unknown as string
);
});
} else if (typeof firstSortProperty === 'number') {
// number in-place sort
if (asc) {
arrayClone.sort(
(a, b) => Number(keyFunction(a)) - Number(keyFunction(b))
);
} else {
arrayClone.sort(
(a, b) => Number(keyFunction(b)) - Number(keyFunction(a))
);
}
} else {
throw new Error('sortBy is not implemented for that type!');
}
return arrayClone;
};
}
if (!Array.prototype.groupBy) {
Array.prototype.groupBy = function <T>(
fn: (item: T) => any,
reduceFn: (items: T[]) => any
): Map<any, any> {
const result = new Map<any, any>();
const distinctKeys = new Set<any>(this.map((x) => fn(x)));
for (const distinctKey of distinctKeys) {
const distinctKeyItems = this.filter((x) => fn(x) === distinctKey);
result.set(distinctKey, reduceFn(distinctKeyItems));
}
return result;
};
}
export {};

View File

@@ -0,0 +1,13 @@
<div class="custom-header">
<lib-icon-button
class="button"
icon="chevron-left"
(click)="onClickCancel()"
></lib-icon-button>
<span class="text">{{ identity?.nick }} </span>
</div>
<div class="edit-identity-outlet">
<router-outlet></router-outlet>
</div>

View File

@@ -0,0 +1,47 @@
:host {
height: 100%;
display: flex;
flex-direction: column;
overflow-y: hidden;
overflow-x: hidden;
.custom-header {
padding-top: var(--size);
padding-bottom: var(--size);
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto;
align-items: center;
background: var(--background);
.button {
grid-column-start: 1;
grid-column-end: 2;
grid-row-start: 1;
grid-row-end: 2;
justify-self: start;
margin-left: 16px;
z-index: 1;
}
.text {
grid-column-start: 1;
grid-column-end: 2;
grid-row-start: 1;
grid-row-end: 2;
font-size: 20px;
font-weight: 500;
justify-self: center;
height: 32px;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 70%;
}
}
.edit-identity-outlet {
flex-grow: 1;
overflow-y: hidden;
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EditIdentityComponent } from './edit-identity.component';
describe('EditIdentityComponent', () => {
let component: EditIdentityComponent;
let fixture: ComponentFixture<EditIdentityComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [EditIdentityComponent]
})
.compileComponents();
fixture = TestBed.createComponent(EditIdentityComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,43 @@
import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { IconButtonComponent, Identity_DECRYPTED, StorageService } from '@common';
@Component({
selector: 'app-edit-identity',
templateUrl: './edit-identity.component.html',
styleUrl: './edit-identity.component.scss',
imports: [RouterOutlet, IconButtonComponent],
})
export class EditIdentityComponent implements OnInit {
identity?: Identity_DECRYPTED;
previousRoute?: string;
readonly #activatedRoute = inject(ActivatedRoute);
readonly #storage = inject(StorageService);
readonly #router = inject(Router);
constructor() {
// Must be called in the constructor and NOT in ngOnInit.
this.previousRoute = this.#router
.getCurrentNavigation()
?.previousNavigation?.extractedUrl.toString();
}
ngOnInit(): void {
const selectedIdentityId = this.#activatedRoute.snapshot.params['id'];
if (!selectedIdentityId) {
return;
}
this.identity = this.#storage
.getBrowserSessionHandler()
.browserSessionData?.identities.find((x) => x.id === selectedIdentityId);
}
onClickCancel() {
if (!this.previousRoute) {
return;
}
this.#router.navigateByUrl(this.previousRoute);
}
}

View File

@@ -0,0 +1,28 @@
<lib-nav-item text="Keys" (click)="onClickNavigateTo('keys')"></lib-nav-item>
<lib-nav-item
text="Relays"
(click)="onClickNavigateTo('relays')"
></lib-nav-item>
<lib-nav-item
text="Permissions"
(click)="onClickNavigateTo('permissions')"
></lib-nav-item>
<div class="sam-flex-grow"></div>
<button
type="button"
class="btn btn-danger"
(click)="
confirm.show(
'Do you really want to delete this identity?',
onConfirmDeletion.bind(this)
)
"
>
Delete Identity
</button>
<lib-confirm #confirm> </lib-confirm>

View File

@@ -0,0 +1,8 @@
:host {
height: 100%;
display: flex;
flex-direction: column;
padding-left: var(--size);
padding-right: var(--size);
padding-bottom: var(--size);
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HomeComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,48 @@
import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import {
ConfirmComponent,
Identity_DECRYPTED,
NavItemComponent,
StorageService,
} from '@common';
@Component({
selector: 'app-edit-identity-home',
imports: [NavItemComponent, ConfirmComponent],
templateUrl: './home.component.html',
styleUrl: './home.component.scss',
})
export class HomeComponent implements OnInit {
identity?: Identity_DECRYPTED;
readonly #activatedRoute = inject(ActivatedRoute);
readonly #storage = inject(StorageService);
readonly #router = inject(Router);
ngOnInit(): void {
const identityId = this.#activatedRoute.parent?.snapshot.params['id'];
if (!identityId) {
return;
}
this.#initialize(identityId);
}
onClickNavigateTo(destination: 'keys' | 'permissions' | 'relays') {
this.#router.navigateByUrl(
`/edit-identity/${this.identity?.id}/${destination}`
);
}
async onConfirmDeletion() {
await this.#storage.deleteIdentity(this.identity?.id);
await this.#router.navigateByUrl('/home/identities');
}
#initialize(selectedIdentityId: string) {
this.identity = this.#storage
.getBrowserSessionHandler()
.browserSessionData?.identities.find((x) => x.id === selectedIdentityId);
}
}

View File

@@ -0,0 +1,141 @@
<div class="header-pane">
<lib-icon-button
icon="chevron-left"
(click)="navigateBack()"
></lib-icon-button>
<span>Keys</span>
</div>
@if(identity) {
<span>Public Key</span>
<!-- PUBKEY NPUB -->
<div class="sam-mt-h sam-flex-row gap">
<span class="text-muted" style="width: 48px">NPUB</span>
<div class="input-group">
<input
id="pubkeyNpubInput"
#pubkeyNpubInput
type="text"
class="form-control"
[ngModel]="identity.pubkeyNpub"
[readOnly]="true"
/>
<button
class="btn btn-outline-secondary"
type="button"
(click)="
copyToClipboard(identity.pubkeyNpub); toast.show('Copied to clipboard')
"
>
<i
class="bi bi-copy"
[class.bi-eye]="pubkeyNpubInput.type === 'password'"
[class.bi-eye-slash]="pubkeyNpubInput.type === 'text'"
></i>
</button>
</div>
</div>
<!-- PUBKEY HEX -->
<div class="sam-mt-h sam-flex-row gap">
<span class="text-muted" style="width: 48px">HEX</span>
<div class="input-group">
<input
id="pubkeyHexInput"
#pubkeyHexInput
type="text"
class="form-control"
[ngModel]="identity.pubkeyHex"
[readOnly]="true"
/>
<button
class="btn btn-outline-secondary"
type="button"
(click)="
copyToClipboard(identity.pubkeyHex); toast.show('Copied to clipboard')
"
>
<i
class="bi bi-copy"
[class.bi-eye]="pubkeyHexInput.type === 'password'"
[class.bi-eye-slash]="pubkeyHexInput.type === 'text'"
></i>
</button>
</div>
</div>
<span class="sam-mt-2">Private Key</span>
<!-- PRIVATE NSEC -->
<div class="sam-mt-h sam-flex-row gap">
<span class="text-muted" style="width: 48px">NSEC</span>
<div class="input-group">
<input
id="privkeyNsecInput"
#privkeyNsecInput
type="password"
class="form-control"
[ngModel]="identity.privkeyNsec"
[readOnly]="true"
/>
<button
class="btn btn-outline-secondary"
type="button"
(click)="
copyToClipboard(identity.privkeyNsec); toast.show('Copied to clipboard')
"
>
<i class="bi bi-copy"></i>
</button>
<button
class="btn btn-outline-secondary"
type="button"
(click)="toggleType(privkeyNsecInput)"
>
<i
class="bi bi-eye"
[class.bi-eye]="privkeyNsecInput.type === 'password'"
[class.bi-eye-slash]="privkeyNsecInput.type === 'text'"
></i>
</button>
</div>
</div>
<!-- PRIVATE HEX -->
<div class="sam-mt-h sam-flex-row gap">
<span class="text-muted" style="width: 48px">HEX</span>
<div class="input-group">
<input
id="privkeyHexInput"
#privkeyHexInput
type="password"
class="form-control"
[ngModel]="identity.privkeyHex"
[readOnly]="true"
/>
<button
class="btn btn-outline-secondary"
type="button"
(click)="
copyToClipboard(identity.privkeyHex); toast.show('Copied to clipboard')
"
>
<i class="bi bi-copy"></i>
</button>
<button
class="btn btn-outline-secondary"
type="button"
(click)="toggleType(privkeyHexInput)"
>
<i
class="bi bi-eye"
[class.bi-eye]="privkeyHexInput.type === 'password'"
[class.bi-eye-slash]="privkeyHexInput.type === 'text'"
></i>
</button>
</div>
</div>
}
<lib-toast #toast [bottom]="16"></lib-toast>

View File

@@ -0,0 +1,19 @@
:host {
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
padding-left: var(--size);
padding-right: var(--size);
.header-pane {
display: flex;
flex-direction: row;
column-gap: var(--size-h);
align-items: center;
padding-bottom: var(--size);
background-color: var(--background);
position: sticky;
top: 0;
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { KeysComponent } from './keys.component';
describe('KeysComponent', () => {
let component: KeysComponent;
let fixture: ComponentFixture<KeysComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [KeysComponent]
})
.compileComponents();
fixture = TestBed.createComponent(KeysComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,74 @@
import { Component, inject, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import {
IconButtonComponent,
NavComponent,
NostrHelper,
StorageService,
ToastComponent,
} from '@common';
interface CustomIdentity {
id: string;
nick: string;
privkeyNsec: string;
privkeyHex: string;
pubkeyNpub: string;
pubkeyHex: string;
}
@Component({
selector: 'app-keys',
imports: [IconButtonComponent, FormsModule, ToastComponent],
templateUrl: './keys.component.html',
styleUrl: './keys.component.scss',
})
export class KeysComponent extends NavComponent implements OnInit {
identity?: CustomIdentity;
readonly #activatedRoute = inject(ActivatedRoute);
readonly #storage = inject(StorageService);
ngOnInit(): void {
const identityId = this.#activatedRoute.parent?.snapshot.params['id'];
if (!identityId) {
return;
}
this.#initialize(identityId);
}
copyToClipboard(text: string) {
navigator.clipboard.writeText(text);
}
toggleType(element: HTMLInputElement) {
if (element.type === 'password') {
element.type = 'text';
} else {
element.type = 'password';
}
}
async #initialize(identityId: string) {
const identity = this.#storage
.getBrowserSessionHandler()
.browserSessionData?.identities.find((x) => x.id === identityId);
if (!identity) {
return;
}
const pubkey = NostrHelper.pubkeyFromPrivkey(identity.privkey);
this.identity = {
id: identity.id,
nick: identity.nick,
privkeyHex: identity.privkey,
privkeyNsec: NostrHelper.privkey2nsec(identity.privkey),
pubkeyHex: pubkey,
pubkeyNpub: NostrHelper.pubkey2npub(pubkey),
};
}
}

View File

@@ -0,0 +1,40 @@
<div class="header-pane">
<lib-icon-button
icon="chevron-left"
(click)="navigateBack()"
></lib-icon-button>
<span>Permissions</span>
</div>
@if(hostsPermissions.length === 0) {
<span class="text-muted" style="font-size: 12px">
Nothing configured so far.
</span>
} @for(hostPermissions of hostsPermissions; track hostPermissions) {
<div class="permissions-card">
<span style="margin-bottom: 4px; font-weight: 500">
{{ hostPermissions.host }}
</span>
@for(permission of hostPermissions.permissions; track permission) {
<div class="permission">
<span
[class.action-allow]="permission.methodPolicy === 'allow'"
[class.action-deny]="permission.methodPolicy === 'deny'"
>{{ permission.methodPolicy }}</span
>
<span class="text-muted">{{ permission.method }}</span>
@if(typeof permission.kind !== 'undefined') {
<span>(kind {{ permission.kind }})</span>
}
<div class="sam-flex-grow"></div>
<lib-icon-button
icon="trash"
title="Revoke permission"
(click)="onClickRevokePermission(permission)"
></lib-icon-button>
</div>
}
</div>
}

View File

@@ -0,0 +1,61 @@
:host {
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
padding-left: var(--size);
padding-right: var(--size);
.header-pane {
display: flex;
flex-direction: row;
column-gap: var(--size-h);
align-items: center;
padding-bottom: var(--size);
background-color: var(--background);
position: sticky;
top: 0;
}
.permissions-card {
background: var(--background-light);
border-radius: 8px;
padding: calc(var(--size) / 2) var(--size);
display: flex;
flex-direction: column;
margin-bottom: 4px;
.permission {
display: flex;
flex-direction: row;
align-items: center;
column-gap: var(--size);
font-size: 12px;
margin-left: -8px;
padding-left: 8px;
margin-right: -8px;
padding-right: 8px;
border-radius: 4px;
&:hover {
background: var(--background-light-hover);
}
.action-allow {
background: var(--bs-green);
border-radius: 4px;
padding: 0 4px;
width: 40px;
text-align: center;
}
.action-deny {
background: var(--bs-danger-border-subtle);
border-radius: 4px;
padding: 0 4px;
width: 40px;
text-align: center;
}
}
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PermissionsComponent } from './permissions.component';
describe('PermissionsComponent', () => {
let component: PermissionsComponent;
let fixture: ComponentFixture<PermissionsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PermissionsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(PermissionsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,75 @@
import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { IconButtonComponent, Identity_DECRYPTED, NavComponent, Permission_DECRYPTED, StorageService } from '@common';
interface HostPermissions {
host: string;
permissions: Permission_DECRYPTED[];
}
@Component({
selector: 'app-permissions',
imports: [IconButtonComponent],
templateUrl: './permissions.component.html',
styleUrl: './permissions.component.scss',
})
export class PermissionsComponent extends NavComponent implements OnInit {
identity?: Identity_DECRYPTED;
hostsPermissions: HostPermissions[] = [];
readonly #activatedRoute = inject(ActivatedRoute);
readonly #storage = inject(StorageService);
ngOnInit(): void {
const selectedIdentityId =
this.#activatedRoute.parent?.snapshot.params['id'];
if (!selectedIdentityId) {
return;
}
this.#initialize(selectedIdentityId);
}
async onClickRevokePermission(permission: Permission_DECRYPTED) {
await this.#storage.deletePermission(permission.id);
this.#buildHostsPermissions(this.identity?.id);
}
#initialize(identityId: string) {
this.identity = this.#storage
.getBrowserSessionHandler()
.browserSessionData?.identities.find((x) => x.id === identityId);
if (!this.identity) {
return;
}
this.#buildHostsPermissions(identityId);
}
#buildHostsPermissions(identityId: string | undefined) {
if (!identityId) {
return;
}
this.hostsPermissions = [];
const hostPermissions = (
this.#storage.getBrowserSessionHandler().browserSessionData
?.permissions ?? []
)
.filter((x) => x.identityId === identityId)
.sortBy((x) => x.host)
.groupBy(
(x) => x.host,
(y) => y
);
hostPermissions.forEach((permissions, host) => {
this.hostsPermissions.push({
host: host,
permissions: permissions.sortBy((x) => x.method),
});
});
}
}

View File

@@ -0,0 +1,79 @@
<!-- RELAY_TEMPLATE -->
<ng-template #relayTemplate let-relay="relay">
<div class="sam-flex-row gap relay">
<div class="sam-flex-column sam-flex-grow">
<span>{{ relay.url | visualRelay }}</span>
<div class="sam-flex-row gap-h">
<lib-relay-rw
type="read"
[(model)]="relay.read"
(modelChange)="onRelayChanged(relay)"
></lib-relay-rw>
<lib-relay-rw
type="write"
[(model)]="relay.write"
(modelChange)="onRelayChanged(relay)"
></lib-relay-rw>
</div>
</div>
<lib-icon-button
icon="trash"
title="Remove relay"
(click)="onClickRemoveRelay(relay)"
style="margin-top: 4px"
></lib-icon-button>
</div>
</ng-template>
<div class="header-pane">
<lib-icon-button
icon="chevron-left"
(click)="navigateBack()"
></lib-icon-button>
<span>Relays</span>
</div>
<div class="sam-mb-2 sam-flex-row gap">
<div class="sam-flex-column sam-flex-grow">
<input
type="text"
(focus)="addRelayInputHasFocus = true"
(blur)="addRelayInputHasFocus = false"
[placeholder]="addRelayInputHasFocus ? 'server.com' : 'Add a relay'"
class="form-control"
[(ngModel)]="newRelay.url"
(ngModelChange)="evaluateCanAdd()"
/>
<div class="sam-flex-row gap-h" style="margin-top: 4px">
<lib-relay-rw
class="sam-flex-grow"
type="read"
[(model)]="newRelay.read"
(modelChange)="evaluateCanAdd()"
></lib-relay-rw>
<lib-relay-rw
class="sam-flex-grow"
type="write"
[(model)]="newRelay.write"
(modelChange)="evaluateCanAdd()"
></lib-relay-rw>
</div>
</div>
<button
type="button"
class="btn btn-primary"
style="height: 100%"
(click)="onClickAddRelay()"
[disabled]="!canAdd"
>
Add
</button>
</div>
@for(relay of relays; track relay) {
<ng-container
*ngTemplateOutlet="relayTemplate; context: { relay: relay }"
></ng-container>
}

View File

@@ -0,0 +1,30 @@
:host {
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
padding-left: var(--size);
padding-right: var(--size);
.header-pane {
display: flex;
flex-direction: row;
column-gap: var(--size-h);
align-items: center;
padding-bottom: var(--size);
background-color: var(--background);
position: sticky;
top: 0;
}
.relay {
margin-bottom: 4px;
padding: 4px 8px 6px 8px;
border-radius: 8px;
background: var(--background-light);
&:hover {
background: var(--background-light-hover);
}
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RelaysComponent } from './relays.component';
describe('RelaysComponent', () => {
let component: RelaysComponent;
let fixture: ComponentFixture<RelaysComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RelaysComponent]
})
.compileComponents();
fixture = TestBed.createComponent(RelaysComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,131 @@
import { NgTemplateOutlet } from '@angular/common';
import { Component, inject, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import {
IconButtonComponent,
Identity_DECRYPTED,
NavComponent,
Relay_DECRYPTED,
RelayRwComponent,
StorageService,
VisualRelayPipe,
} from '@common';
interface NewRelay {
url: string;
read: boolean;
write: boolean;
}
@Component({
selector: 'app-relays',
imports: [
IconButtonComponent,
FormsModule,
RelayRwComponent,
NgTemplateOutlet,
VisualRelayPipe,
],
templateUrl: './relays.component.html',
styleUrl: './relays.component.scss',
})
export class RelaysComponent extends NavComponent implements OnInit {
identity?: Identity_DECRYPTED;
relays: Relay_DECRYPTED[] = [];
addRelayInputHasFocus = false;
newRelay: NewRelay = {
url: '',
read: true,
write: true,
};
canAdd = false;
readonly #activatedRoute = inject(ActivatedRoute);
readonly #storage = inject(StorageService);
ngOnInit(): void {
const selectedIdentityId =
this.#activatedRoute.parent?.snapshot.params['id'];
if (!selectedIdentityId) {
return;
}
this.#loadData(selectedIdentityId);
}
evaluateCanAdd() {
let canAdd = true;
if (!this.newRelay.url) {
canAdd = false;
} else if (!this.newRelay.read && !this.newRelay.write) {
canAdd = false;
}
this.canAdd = canAdd;
}
async onClickRemoveRelay(relay: Relay_DECRYPTED) {
if (!this.identity) {
return;
}
try {
await this.#storage.deleteRelay(relay.id);
this.#loadData(this.identity.id);
} catch (error) {
console.log(error);
// TODO
}
}
async onClickAddRelay() {
if (!this.identity) {
return;
}
try {
await this.#storage.addRelay({
identityId: this.identity.id,
url: 'wss://' + this.newRelay.url.toLowerCase(),
read: this.newRelay.read,
write: this.newRelay.write,
});
this.newRelay = {
url: '',
read: true,
write: true,
};
this.evaluateCanAdd();
this.#loadData(this.identity.id);
} catch (error) {
console.log(error);
// TODO
}
}
async onRelayChanged(relay: Relay_DECRYPTED) {
try {
await this.#storage.updateRelay(relay);
} catch (error) {
console.log(error);
// TODO
}
}
#loadData(identityId: string) {
this.identity = this.#storage
.getBrowserSessionHandler()
.browserSessionData?.identities.find((x) => x.id === identityId);
const relays: Relay_DECRYPTED[] = [];
(this.#storage.getBrowserSessionHandler().browserSessionData?.relays ?? [])
.filter((x) => x.identityId === identityId)
.forEach((x) => {
relays.push(JSON.parse(JSON.stringify(x)));
});
this.relays = relays;
}
}

View File

@@ -0,0 +1,36 @@
<div class="tab-content">
<router-outlet></router-outlet>
</div>
<div class="tabs">
<a
class="tab"
routerLink="/home/identity"
routerLinkActive="active"
title="Your selected identity"
>
<i class="bi bi-person-circle"></i>
</a>
<a
class="tab"
routerLink="/home/identities"
routerLinkActive="active"
title="Identities"
>
<i class="bi bi-people-fill"></i>
</a>
<a
class="tab"
routerLink="/home/settings"
routerLinkActive="active"
title="Settings"
>
<i class="bi bi-gear"></i>
</a>
<a class="tab" routerLink="/home/info" routerLinkActive="active" title="Info">
<i class="bi bi-info-circle"></i>
</a>
</div>

View File

@@ -0,0 +1,43 @@
:host {
height: 100%;
display: flex;
flex-direction: column;
.tab-content {
height: calc(100% - 60px);
}
.tabs {
height: 60px;
min-height: 60px;
background: var(--background-light);
display: flex;
flex-direction: row;
a {
all: unset;
}
.tab {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: gray;
border-top: 3px solid transparent;
cursor: pointer;
&:hover {
background: var(--background-light-hover);
}
&.active {
color: #ffffff;
border-top: 3px solid #0d6efd;
}
}
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HomeComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { RouterModule, RouterOutlet } from '@angular/router';
@Component({
selector: 'app-home',
imports: [RouterOutlet, RouterModule],
templateUrl: './home.component.html',
styleUrl: './home.component.scss',
})
export class HomeComponent {}

View File

@@ -0,0 +1,78 @@
<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->
<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->
<div class="custom-header" style="position: sticky; top: 0">
<span class="text">Identities </span>
<button class="button btn btn-primary btn-sm" (click)="onClickNewIdentity()">
<div class="sam-flex-row gap-h">
<i class="bi bi-plus-lg"></i>
<span>New</span>
</div>
</button>
</div>
@let sessionData = storage.getBrowserSessionHandler().browserSessionData;
<!-- - -->
@let identities = sessionData?.identities ?? []; @if(identities.length === 0) {
<div
style="
height: 100%;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
"
>
<span class="sam-text-muted">
Create your first identity by clicking on the button in the upper right
corner.
</span>
</div>
} @for(identity of identities; track identity) {
<div
class="identity"
style="overflow: hidden"
(click)="onClickEditIdentity(identity)"
>
@let isSelected = identity.id === sessionData?.selectedIdentityId;
<span
class="no-select"
style="overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap"
[class.not-active]="!isSelected"
>
{{ identity.nick }}
</span>
<div class="sam-flex-grow"></div>
@if(isSelected) {
<lib-icon-button
icon="star-fill"
title="Edit identity"
style="pointer-events: none; color: var(--bs-pink)"
></lib-icon-button>
}
<div class="buttons sam-flex-row gap-h">
@if(!isSelected) {
<lib-icon-button
icon="star-fill"
title="Select identity"
(click)="
onClickSwitchIdentity(identity.id, $event);
toast.show('Identity changed')
"
></lib-icon-button>
}
</div>
<lib-icon-button
icon="arrow-right"
title="Edit identity"
style="pointer-events: none"
></lib-icon-button>
</div>
}
<lib-toast #toast></lib-toast>

View File

@@ -0,0 +1,68 @@
:host {
height: 100%;
display: flex;
flex-direction: column;
overflow-y: auto;
padding-left: var(--size);
padding-right: var(--size);
.custom-header {
padding-top: var(--size);
padding-bottom: var(--size);
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto;
align-items: center;
background: var(--background);
.button {
grid-column-start: 1;
grid-column-end: 2;
grid-row-start: 1;
grid-row-end: 2;
justify-self: end;
}
.text {
grid-column-start: 1;
grid-column-end: 2;
grid-row-start: 1;
grid-row-end: 2;
font-size: 20px;
font-weight: 500;
justify-self: center;
height: 32px;
}
}
.identity {
height: 48px;
min-height: 48px;
display: flex;
flex-direction: row;
align-items: center;
padding-left: 16px;
padding-right: 8px;
background: var(--background-light);
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
.not-active {
//color: #525b6a;
opacity: 0.4;
}
&:hover {
background: var(--background-light-hover);
.buttons {
visibility: visible;
}
}
.buttons {
visibility: hidden;
}
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IdentitiesComponent } from './identities.component';
describe('IdentitiesComponent', () => {
let component: IdentitiesComponent;
let fixture: ComponentFixture<IdentitiesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [IdentitiesComponent]
})
.compileComponents();
fixture = TestBed.createComponent(IdentitiesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,33 @@
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import {
IconButtonComponent,
Identity_DECRYPTED,
StorageService,
ToastComponent,
} from '@common';
@Component({
selector: 'app-identities',
imports: [IconButtonComponent, ToastComponent],
templateUrl: './identities.component.html',
styleUrl: './identities.component.scss',
})
export class IdentitiesComponent {
readonly storage = inject(StorageService);
readonly #router = inject(Router);
onClickNewIdentity() {
this.#router.navigateByUrl('/new-identity');
}
onClickEditIdentity(identity: Identity_DECRYPTED) {
this.#router.navigateByUrl(`/edit-identity/${identity.id}/home`);
}
async onClickSwitchIdentity(identityId: string, event: MouseEvent) {
event.stopPropagation();
await this.storage.switchIdentity(identityId);
}
}

View File

@@ -0,0 +1,56 @@
<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->
<div class="sam-text-header">
<span>You</span>
</div>
<div class="vertically-centered">
<div class="sam-flex-column center">
<div class="sam-flex-column gap center">
<div class="picture-frame" [class.padding]="!loadedData.profile?.image">
<img
[src]="
!loadedData.profile?.image
? 'person-fill.svg'
: loadedData.profile?.image
"
alt=""
/>
</div>
<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->
<span class="name" (click)="onClickShowDetails()">
{{ selectedIdentity?.nick }}
</span>
@if(loadedData.profile) {
<div class="sam-flex-row gap-h">
@if(loadedData.validating) {
<i class="bi bi-circle color-activity"></i>
} @else { @if(loadedData.nip05isValidated) {
<i class="bi bi-patch-check sam-color-primary"></i>
} @else {
<i class="bi bi-exclamation-octagon-fill sam-color-danger"></i>
} }
<span class="sam-color-primary">{{
loadedData.profile.nip05 | visualNip05
}}</span>
</div>
} @else {
<span>&nbsp;</span>
}
<lib-pubkey
[value]="selectedIdentityNpub ?? 'na'"
[first]="14"
[last]="8"
(click)="
copyToClipboard(selectedIdentityNpub);
toast.show('Copied to clipboard')
"
></lib-pubkey>
</div>
</div>
</div>
<lib-toast #toast></lib-toast>

View File

@@ -0,0 +1,41 @@
:host {
height: 100%;
display: flex;
flex-direction: column;
.vertically-centered {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.name {
font-size: 20px;
font-weight: 500;
cursor: pointer;
max-width: 343px;
overflow-x: hidden;
text-overflow: ellipsis;
}
.picture-frame {
height: 120px;
width: 120px;
border: 2px solid white;
border-radius: 100%;
&.padding {
padding: 12px;
}
img {
border-radius: 100%;
width: 100%;
height: 100%;
}
}
.color-activity {
color: var(--bs-border-color);
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IdentityComponent } from './identity.component';
describe('IdentityComponent', () => {
let component: IdentityComponent;
let fixture: ComponentFixture<IdentityComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [IdentityComponent]
})
.compileComponents();
fixture = TestBed.createComponent(IdentityComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,117 @@
import { Component, inject, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import {
Identity_DECRYPTED,
NostrHelper,
PubkeyComponent,
StorageService,
ToastComponent,
VisualNip05Pipe,
} from '@common';
import NDK, { NDKUserProfile } from '@nostr-dev-kit/ndk';
interface LoadedData {
profile: NDKUserProfile | undefined;
nip05: string | undefined;
nip05isValidated: boolean | undefined;
validating: boolean;
}
@Component({
selector: 'app-identity',
imports: [PubkeyComponent, VisualNip05Pipe, ToastComponent],
templateUrl: './identity.component.html',
styleUrl: './identity.component.scss',
})
export class IdentityComponent implements OnInit {
selectedIdentity: Identity_DECRYPTED | undefined;
selectedIdentityNpub: string | undefined;
loadedData: LoadedData = {
profile: undefined,
nip05: undefined,
nip05isValidated: undefined,
validating: false,
};
readonly #storage = inject(StorageService);
readonly #router = inject(Router);
ngOnInit(): void {
this.#loadData();
}
copyToClipboard(pubkey: string | undefined) {
if (!pubkey) {
return;
}
navigator.clipboard.writeText(pubkey);
}
onClickShowDetails() {
if (!this.selectedIdentity) {
return;
}
this.#router.navigateByUrl(
`/edit-identity/${this.selectedIdentity.id}/home`
);
}
async #loadData() {
try {
const selectedIdentityId =
this.#storage.getBrowserSessionHandler().browserSessionData
?.selectedIdentityId ?? null;
const identity = this.#storage
.getBrowserSessionHandler()
.browserSessionData?.identities.find(
(x) => x.id === selectedIdentityId
);
if (!identity) {
return;
}
this.selectedIdentity = identity;
const pubkey = NostrHelper.pubkeyFromPrivkey(identity.privkey);
this.selectedIdentityNpub = NostrHelper.pubkey2npub(pubkey);
// Determine the user's relays to check for his profile.
const relays =
this.#storage
.getBrowserSessionHandler()
.browserSessionData?.relays.filter(
(x) => x.identityId === identity.id
) ?? [];
if (relays.length === 0) {
return;
}
const relevantRelays = relays.filter((x) => x.write).map((x) => x.url);
// Fetch the user's profile.
const ndk = new NDK({
explicitRelayUrls: relevantRelays,
});
await ndk.connect();
const user = ndk.getUser({
pubkey: NostrHelper.pubkeyFromPrivkey(identity.privkey),
//relayUrls: relevantRelays,
});
this.loadedData.profile = (await user.fetchProfile()) ?? undefined;
if (this.loadedData.profile?.nip05) {
this.loadedData.validating = true;
this.loadedData.nip05isValidated =
(await user.validateNip05(this.loadedData.profile.nip05)) ??
undefined;
this.loadedData.validating = false;
}
} catch (error) {
console.error(error);
// TODO
}
}
}

View File

@@ -0,0 +1,34 @@
<div class="sam-text-header">
<span> Gooti </span>
</div>
<span>Version {{ version }}</span>
<span>&nbsp;</span>
<span> Website </span>
<a href="https://getgooti.com" target="_blank">www.getgooti.com</a>
<span>&nbsp;</span>
<span> Source code</span>
<a href="https://github.com/sam-hayes-org/gooti-extension" target="_blank">
github.com/sam-hayes-org/gooti-extension
</a>
<div class="sam-flex-grow"></div>
<div class="sam-card sam-mb" style="align-items: center">
<span>
Made with <i class="bi bi-heart-fill" style="color: red"></i> by
<a href="https://sam-hayes.org" target="_blank">Sam Hayes</a>
</span>
<lib-pubkey
class="sam-mt-h"
value="npub1tgyjshvelwj73t3jy0n3xllgt03elkapfl3k3n0x2wkunegkgrwssfp0u4"
(click)="toast.show('Copied to clipboard')"
></lib-pubkey>
</div>
<lib-toast #toast [bottom]="188"></lib-toast>

View File

@@ -0,0 +1,9 @@
:host {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
overflow-y: auto;
padding-left: var(--size);
padding-right: var(--size);
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InfoComponent } from './info.component';
describe('InfoComponent', () => {
let component: InfoComponent;
let fixture: ComponentFixture<InfoComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [InfoComponent]
})
.compileComponents();
fixture = TestBed.createComponent(InfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,13 @@
import { Component } from '@angular/core';
import { PubkeyComponent, ToastComponent } from '@common';
import packageJson from '../../../../../../../package.json';
@Component({
selector: 'app-info',
imports: [PubkeyComponent, ToastComponent],
templateUrl: './info.component.html',
styleUrl: './info.component.scss',
})
export class InfoComponent {
version = packageJson.custom.firefox.version;
}

View File

@@ -0,0 +1,29 @@
<div class="sam-text-header">
<span> Settings </span>
</div>
<span>SYNC: {{ syncFlow }}</span>
<button class="btn btn-primary" (click)="onClickExportVault()">
Export Vault
</button>
<button class="btn btn-primary" (click)="navigate('/vault-import')">
Import Vault
</button>
<div class="sam-flex-grow"></div>
<button
class="btn btn-danger"
(click)="
confirm.show(
'Do you really want to reset your extension? All data will be lost.',
onResetExtension.bind(this)
)
"
>
Reset Extension
</button>
<lib-confirm #confirm> </lib-confirm>

View File

@@ -0,0 +1,14 @@
:host {
height: 100%;
display: flex;
flex-direction: column;
row-gap: var(--size);
overflow-y: auto;
padding-left: var(--size);
padding-right: var(--size);
.file-input {
position: absolute;
visibility: hidden;
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsComponent } from './settings.component';
describe('SettingsComponent', () => {
let component: SettingsComponent;
let fixture: ComponentFixture<SettingsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SettingsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,73 @@
import { Component, inject, OnInit } from '@angular/core';
import {
BrowserSyncFlow,
ConfirmComponent,
DateHelper,
NavComponent,
StartupService,
StorageService,
} from '@common';
import { getNewStorageServiceConfig } from '../../../common/data/get-new-storage-service-config';
@Component({
selector: 'app-settings',
imports: [ConfirmComponent],
templateUrl: './settings.component.html',
styleUrl: './settings.component.scss',
})
export class SettingsComponent extends NavComponent implements OnInit {
syncFlow: string | undefined;
readonly #storage = inject(StorageService);
readonly #startup = inject(StartupService);
ngOnInit(): void {
const vault = JSON.stringify(
this.#storage.getBrowserSyncHandler().browserSyncData
);
console.log(vault.length / 1024 + ' KB');
switch (this.#storage.getGootiMetaHandler().gootiMetaData?.syncFlow) {
case BrowserSyncFlow.NO_SYNC:
this.syncFlow = 'Off';
break;
case BrowserSyncFlow.BROWSER_SYNC:
this.syncFlow = 'Mozilla Firefox';
break;
default:
break;
}
}
async onResetExtension() {
try {
await this.#storage.resetExtension();
this.#startup.startOver(getNewStorageServiceConfig());
} catch (error) {
console.log(error);
// TODO
}
}
async onClickExportVault() {
const jsonVault = this.#storage.exportVault();
const dateTimeString = DateHelper.dateToISOLikeButLocal(new Date());
const fileName = `Gooti Chrome - Vault Export - ${dateTimeString}.json`;
this.#downloadJson(jsonVault, fileName);
}
#downloadJson(jsonString: string, fileName: string) {
const dataStr =
'data:text/json;charset=utf-8,' + encodeURIComponent(jsonString);
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute('href', dataStr);
downloadAnchorNode.setAttribute('download', fileName);
document.body.appendChild(downloadAnchorNode);
downloadAnchorNode.click();
downloadAnchorNode.remove();
}
}

View File

@@ -0,0 +1,85 @@
<div class="sam-text-header">
<span>New Identity</span>
</div>
<div class="content">
<input
id="nickElement"
type="text"
placeholder="Nick"
class="form-control form-control-lg"
style="font-size: 1rem"
[(ngModel)]="identity.nick"
autocomplete="off"
(ngModelChange)="validateCanSave()"
/>
<div class="sam-mt input-group mb-3">
<input
id="privkeyInputElement"
#privkeyInputElement
type="password"
placeholder="Private Key (HEX or NSEC)"
class="form-control form-control-lg"
style="font-size: 1rem"
[(ngModel)]="identity.privkeyInput"
autocomplete="off"
(ngModelChange)="validateCanSave()"
/>
<button
class="btn btn-outline-secondary"
type="button"
(click)="toggleType(privkeyInputElement)"
>
<i
class="bi bi-eye"
[class.bi-eye]="privkeyInputElement.type === 'password'"
[class.bi-eye-slash]="privkeyInputElement.type === 'text'"
></i>
</button>
</div>
<button
class="sam-mt"
(click)="onClickGeneratePrivkey()"
type="button"
class="btn btn-link"
>
Generate private key
</button>
</div>
<div class="sam-footer-grid-2">
<button type="button" class="btn btn-secondary" (click)="navigateBack()">
Cancel
</button>
<button
[disabled]="!canSave"
type="button"
class="btn btn-primary"
(click)="onClickSave()"
>
Save
</button>
</div>
<!----------->
<!-- ALERT -->
<!----------->
@if(alertMessage) {
<div
style="
position: absolute;
bottom: 60px;
align-self: center;
margin-left: 16px;
margin-right: 16px;
"
>
<div class="alert alert-danger sam-flex-row gap" role="alert">
<i class="bi bi-exclamation-triangle"></i>
<span>{{ alertMessage }}</span>
</div>
</div>
}

View File

@@ -0,0 +1,13 @@
:host {
height: 100%;
display: flex;
flex-direction: column;
.content {
padding-left: var(--size);
padding-right: var(--size);
flex-grow: 1;
display: flex;
flex-direction: column;
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NewIdentityComponent } from './new-identity.component';
describe('NewIdentityComponent', () => {
let component: NewIdentityComponent;
let fixture: ComponentFixture<NewIdentityComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NewIdentityComponent]
})
.compileComponents();
fixture = TestBed.createComponent(NewIdentityComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

Some files were not shown because too many files have changed in this diff Show More