Compare commits
17 Commits
chrome-0.0
...
firefox-0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53ec023218 | ||
|
|
bd8bd101d7 | ||
|
|
e85ac5ca66 | ||
|
|
0a77eceaf4 | ||
|
|
6c43a60810 | ||
|
|
b20faf2359 | ||
|
|
601ac8cd49 | ||
|
|
27e8d52d23 | ||
|
|
7f0829af09 | ||
|
|
3ec8827c27 | ||
|
|
f2960743e1 | ||
|
|
d3d76efc83 | ||
|
|
4b7d8d4a8d | ||
|
|
a973b1edad | ||
|
|
312a3666d7 | ||
|
|
142b313867 | ||
|
|
b21701f677 |
@@ -1 +1 @@
|
||||
npm run lint
|
||||
# npm run lint
|
||||
|
||||
29
angular.json
29
angular.json
@@ -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
7
firefox_prepare_manifest.sh
Executable 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
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
11
package.json
11
package.json
@@ -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"
|
||||
|
||||
@@ -18,5 +18,9 @@ module.exports = {
|
||||
import: 'src/prompt.ts',
|
||||
runtime: false,
|
||||
},
|
||||
options: {
|
||||
import: 'src/options.ts',
|
||||
runtime: false,
|
||||
},
|
||||
},
|
||||
} as Configuration;
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
173
projects/chrome/public/options.html
Normal file
173
projects/chrome/public/options.html
Normal 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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
<span>Version {{ version }}</span>
|
||||
|
||||
<br />
|
||||
<span> </span>
|
||||
|
||||
<span> Website </span>
|
||||
<a href="https://getgooti.com" target="_blank">www.getgooti.com</a>
|
||||
|
||||
<br />
|
||||
<span> </span>
|
||||
|
||||
<span> Source code</span>
|
||||
<a
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
130
projects/chrome/src/options.ts
Normal file
130
projects/chrome/src/options.ts
Normal 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);
|
||||
});
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
margin-top: var(--size-h);
|
||||
}
|
||||
|
||||
.sam-mt-hh {
|
||||
margin-top: var(--size-hh);
|
||||
}
|
||||
|
||||
.sam-mb {
|
||||
margin-bottom: var(--size);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
--size-2: 32px;
|
||||
--size: 16px;
|
||||
--size-h: 8px;
|
||||
--size-hh: 4px;
|
||||
|
||||
--background: #161c26;
|
||||
--background-light: #202733;
|
||||
|
||||
@@ -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';
|
||||
|
||||
26
projects/firefox/custom-webpack.config.ts
Normal file
26
projects/firefox/custom-webpack.config.ts
Normal 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;
|
||||
1
projects/firefox/public/bird.svg
Normal file
1
projects/firefox/public/bird.svg
Normal 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 |
BIN
projects/firefox/public/gooti-with-bg.png
Normal file
BIN
projects/firefox/public/gooti-with-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 983 B |
1
projects/firefox/public/gooti.svg
Normal file
1
projects/firefox/public/gooti.svg
Normal 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 |
47
projects/firefox/public/manifest.json
Normal file
47
projects/firefox/public/manifest.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
173
projects/firefox/public/options.html
Normal file
173
projects/firefox/public/options.html
Normal 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>
|
||||
3
projects/firefox/public/person-fill.svg
Normal file
3
projects/firefox/public/person-fill.svg
Normal 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 |
221
projects/firefox/public/prompt.html
Normal file
221
projects/firefox/public/prompt.html
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
39
projects/firefox/src/app/common/data/firefox-meta-handler.ts
Normal file
39
projects/firefox/src/app/common/data/firefox-meta-handler.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
95
projects/firefox/src/app/common/extensions/array.ts
Normal file
95
projects/firefox/src/app/common/extensions/array.ts
Normal 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 {};
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,8 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: var(--size);
|
||||
padding-right: var(--size);
|
||||
padding-bottom: var(--size);
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
36
projects/firefox/src/app/components/home/home.component.html
Normal file
36
projects/firefox/src/app/components/home/home.component.html
Normal 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>
|
||||
43
projects/firefox/src/app/components/home/home.component.scss
Normal file
43
projects/firefox/src/app/components/home/home.component.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
10
projects/firefox/src/app/components/home/home.component.ts
Normal file
10
projects/firefox/src/app/components/home/home.component.ts
Normal 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 {}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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> </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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<div class="sam-text-header">
|
||||
<span> Gooti </span>
|
||||
</div>
|
||||
|
||||
<span>Version {{ version }}</span>
|
||||
|
||||
<span> </span>
|
||||
|
||||
<span> Website </span>
|
||||
<a href="https://getgooti.com" target="_blank">www.getgooti.com</a>
|
||||
|
||||
<span> </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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user