"copy" UI related things from chrome

This commit is contained in:
DEV Sam Hayes
2025-02-04 20:19:30 +01:00
parent 601ac8cd49
commit b20faf2359
100 changed files with 3514 additions and 362 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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