diff options
Diffstat (limited to 'client/src')
16 files changed, 231 insertions, 3 deletions
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html index 9f139b4f2..4c5b46d5b 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html | |||
@@ -4,6 +4,8 @@ | |||
4 | <span class="badge badge-secondary">{{ totalItems }}</span> | 4 | <span class="badge badge-secondary">{{ totalItems }}</span> |
5 | </h1> | 5 | </h1> |
6 | 6 | ||
7 | <my-channels-setup-message [hideLink]="true"></my-channels-setup-message> | ||
8 | |||
7 | <div class="video-channels-header d-flex justify-content-between"> | 9 | <div class="video-channels-header d-flex justify-content-between"> |
8 | <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> | 10 | <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> |
9 | 11 | ||
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html index 7f5b8bbf4..25b742bff 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html | |||
@@ -3,6 +3,8 @@ | |||
3 | <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span> | 3 | <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span> |
4 | </h1> | 4 | </h1> |
5 | 5 | ||
6 | <my-channels-setup-message></my-channels-setup-message> | ||
7 | |||
6 | <div class="video-playlists-header d-flex justify-content-between"> | 8 | <div class="video-playlists-header d-flex justify-content-between"> |
7 | <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> | 9 | <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> |
8 | 10 | ||
diff --git a/client/src/app/+videos/+video-edit/video-add.component.html b/client/src/app/+videos/+video-edit/video-add.component.html index b056c6e7a..62b42ae10 100644 --- a/client/src/app/+videos/+video-edit/video-add.component.html +++ b/client/src/app/+videos/+video-edit/video-add.component.html | |||
@@ -45,6 +45,8 @@ | |||
45 | <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container> | 45 | <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container> |
46 | </div> | 46 | </div> |
47 | 47 | ||
48 | <my-channels-setup-message></my-channels-setup-message> | ||
49 | |||
48 | <div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" [ngClass]="{ 'hide-nav': !!secondStepType }"> | 50 | <div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" [ngClass]="{ 'hide-nav': !!secondStepType }"> |
49 | <ng-container ngbNavItem="upload"> | 51 | <ng-container ngbNavItem="upload"> |
50 | <a ngbNavLink> | 52 | <a ngbNavLink> |
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index 108b127be..da66e4ef0 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html | |||
@@ -60,9 +60,10 @@ | |||
60 | </ng-template> | 60 | </ng-template> |
61 | </p-toast> | 61 | </p-toast> |
62 | 62 | ||
63 | <ng-template [ngIf]="isUserLoggedIn()"> | 63 | <ng-container *ngIf="isUserLoggedIn()"> |
64 | <my-account-setup-modal #accountSetupModal></my-account-setup-modal> | ||
64 | <my-welcome-modal #welcomeModal></my-welcome-modal> | 65 | <my-welcome-modal #welcomeModal></my-welcome-modal> |
65 | <my-instance-config-warning-modal #instanceConfigWarningModal></my-instance-config-warning-modal> | 66 | <my-instance-config-warning-modal #instanceConfigWarningModal></my-instance-config-warning-modal> |
66 | </ng-template> | 67 | </ng-container> |
67 | 68 | ||
68 | <my-custom-modal #customModal></my-custom-modal> | 69 | <my-custom-modal #customModal></my-custom-modal> |
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index dcc1f259f..4d5a9f75f 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts | |||
@@ -20,6 +20,7 @@ import { PluginService } from '@app/core/plugins/plugin.service' | |||
20 | import { CustomModalComponent } from '@app/modal/custom-modal.component' | 20 | import { CustomModalComponent } from '@app/modal/custom-modal.component' |
21 | import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component' | 21 | import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component' |
22 | import { WelcomeModalComponent } from '@app/modal/welcome-modal.component' | 22 | import { WelcomeModalComponent } from '@app/modal/welcome-modal.component' |
23 | import { AccountSetupModalComponent } from '@app/modal/account-setup-modal.component' | ||
23 | import { NgbConfig, NgbModal } from '@ng-bootstrap/ng-bootstrap' | 24 | import { NgbConfig, NgbModal } from '@ng-bootstrap/ng-bootstrap' |
24 | import { LoadingBarService } from '@ngx-loading-bar/core' | 25 | import { LoadingBarService } from '@ngx-loading-bar/core' |
25 | import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' | 26 | import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' |
@@ -37,6 +38,7 @@ import { InstanceService } from './shared/shared-instance' | |||
37 | export class AppComponent implements OnInit, AfterViewInit { | 38 | export class AppComponent implements OnInit, AfterViewInit { |
38 | private static BROADCAST_MESSAGE_KEY = 'app-broadcast-message-dismissed' | 39 | private static BROADCAST_MESSAGE_KEY = 'app-broadcast-message-dismissed' |
39 | 40 | ||
41 | @ViewChild('accountSetupModal') accountSetupModal: AccountSetupModalComponent | ||
40 | @ViewChild('welcomeModal') welcomeModal: WelcomeModalComponent | 42 | @ViewChild('welcomeModal') welcomeModal: WelcomeModalComponent |
41 | @ViewChild('instanceConfigWarningModal') instanceConfigWarningModal: InstanceConfigWarningModalComponent | 43 | @ViewChild('instanceConfigWarningModal') instanceConfigWarningModal: InstanceConfigWarningModalComponent |
42 | @ViewChild('customModal') customModal: CustomModalComponent | 44 | @ViewChild('customModal') customModal: CustomModalComponent |
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index dfdccbe69..8d9c39ad8 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts | |||
@@ -18,6 +18,7 @@ import { CustomModalComponent } from './modal/custom-modal.component' | |||
18 | import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component' | 18 | import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component' |
19 | import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component' | 19 | import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component' |
20 | import { WelcomeModalComponent } from './modal/welcome-modal.component' | 20 | import { WelcomeModalComponent } from './modal/welcome-modal.component' |
21 | import { AccountSetupModalComponent } from './modal/account-setup-modal.component' | ||
21 | import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module' | 22 | import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module' |
22 | import { SharedFormModule } from './shared/shared-forms' | 23 | import { SharedFormModule } from './shared/shared-forms' |
23 | import { SharedGlobalIconModule } from './shared/shared-icons' | 24 | import { SharedGlobalIconModule } from './shared/shared-icons' |
@@ -53,6 +54,7 @@ export function loadConfigFactory (server: ServerService, pluginService: PluginS | |||
53 | SuggestionComponent, | 54 | SuggestionComponent, |
54 | HighlightPipe, | 55 | HighlightPipe, |
55 | 56 | ||
57 | AccountSetupModalComponent, | ||
56 | CustomModalComponent, | 58 | CustomModalComponent, |
57 | WelcomeModalComponent, | 59 | WelcomeModalComponent, |
58 | InstanceConfigWarningModalComponent, | 60 | InstanceConfigWarningModalComponent, |
diff --git a/client/src/app/modal/account-setup-modal.component.html b/client/src/app/modal/account-setup-modal.component.html new file mode 100644 index 000000000..55bae78bf --- /dev/null +++ b/client/src/app/modal/account-setup-modal.component.html | |||
@@ -0,0 +1,33 @@ | |||
1 | <ng-template #modal let-hide="close"> | ||
2 | <div class="modal-header"> | ||
3 | <h4 i18n class="modal-title">Welcome to {{ instanceName }}, dear user!</h4> | ||
4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | ||
5 | </div> | ||
6 | |||
7 | <div class="modal-body"> | ||
8 | <img class="mascot" src="/client/assets/images/mascot/happy.svg" alt="mascot"> | ||
9 | |||
10 | <div i18n class="subtitle">It's time to set up your account profile!</div> | ||
11 | |||
12 | <p i18n>Help moderators and other users to know <strong>who you are</strong> by:</p> | ||
13 | |||
14 | <ul> | ||
15 | <li *ngIf="!hasAccountAvatar" i18n>Uploading an <strong>avatar</strong></li> | ||
16 | <li *ngIf="!hasAccountDescription" i18n>Writing a <strong>description</strong></li> | ||
17 | </ul> | ||
18 | </div> | ||
19 | |||
20 | <div class="modal-footer inputs"> | ||
21 | <input | ||
22 | type="button" role="button" i18n-value value="Remind me later" class="peertube-button grey-button" | ||
23 | (click)="hide()" (key.enter)="hide()" | ||
24 | > | ||
25 | |||
26 | <a i18n (click)="hide()" (key.enter)="hide()" | ||
27 | class="peertube-button-link orange-button" routerLink="/my-account" | ||
28 | rel="noopener noreferrer" ngbAutofocus> | ||
29 | Set up | ||
30 | </a> | ||
31 | </div> | ||
32 | |||
33 | </ng-template> | ||
diff --git a/client/src/app/modal/account-setup-modal.component.scss b/client/src/app/modal/account-setup-modal.component.scss new file mode 100644 index 000000000..405f29d65 --- /dev/null +++ b/client/src/app/modal/account-setup-modal.component.scss | |||
@@ -0,0 +1,31 @@ | |||
1 | @use '_mixins' as *; | ||
2 | @use '_variables' as *; | ||
3 | |||
4 | .modal-body { | ||
5 | font-size: 15px; | ||
6 | display: flex; | ||
7 | flex-direction: column; | ||
8 | align-items: center; | ||
9 | justify-content: center; | ||
10 | } | ||
11 | |||
12 | .mascot-fw { | ||
13 | width: 170px; | ||
14 | } | ||
15 | |||
16 | .mascot { | ||
17 | @include margin-right(2rem); | ||
18 | |||
19 | display: block; | ||
20 | min-width: 170px; | ||
21 | } | ||
22 | |||
23 | .subtitle { | ||
24 | font-weight: $font-semibold; | ||
25 | margin-bottom: 10px; | ||
26 | font-size: 16px; | ||
27 | } | ||
28 | |||
29 | li { | ||
30 | margin-bottom: 10px; | ||
31 | } | ||
diff --git a/client/src/app/modal/account-setup-modal.component.ts b/client/src/app/modal/account-setup-modal.component.ts new file mode 100644 index 000000000..e5d36e006 --- /dev/null +++ b/client/src/app/modal/account-setup-modal.component.ts | |||
@@ -0,0 +1,70 @@ | |||
1 | import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' | ||
2 | import { AuthService, ServerService, User } from '@app/core' | ||
3 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | ||
4 | import { HTMLServerConfig } from '@shared/models' | ||
5 | |||
6 | @Component({ | ||
7 | selector: 'my-account-setup-modal', | ||
8 | templateUrl: './account-setup-modal.component.html', | ||
9 | styleUrls: [ './account-setup-modal.component.scss' ] | ||
10 | }) | ||
11 | export class AccountSetupModalComponent implements OnInit { | ||
12 | @ViewChild('modal', { static: true }) modal: ElementRef | ||
13 | |||
14 | user: User = null | ||
15 | ref: NgbModalRef = null | ||
16 | |||
17 | private serverConfig: HTMLServerConfig | ||
18 | |||
19 | constructor ( | ||
20 | private authService: AuthService, | ||
21 | private modalService: NgbModal, | ||
22 | private serverService: ServerService | ||
23 | ) { } | ||
24 | |||
25 | get userInformationLoaded () { | ||
26 | return this.authService.userInformationLoaded | ||
27 | } | ||
28 | |||
29 | get instanceName () { | ||
30 | return this.serverConfig.instance.name | ||
31 | } | ||
32 | |||
33 | get isUserRoot () { | ||
34 | return this.user.username === 'root' | ||
35 | } | ||
36 | |||
37 | get hasAccountAvatar () { | ||
38 | return !!this.user.account.avatar | ||
39 | } | ||
40 | |||
41 | get hasAccountDescription () { | ||
42 | return !!this.user.account.description | ||
43 | } | ||
44 | |||
45 | ngOnInit () { | ||
46 | this.serverConfig = this.serverService.getHTMLConfig() | ||
47 | this.user = this.authService.getUser() | ||
48 | |||
49 | this.authService.userInformationLoaded | ||
50 | .subscribe( | ||
51 | () => { | ||
52 | if (this.isUserRoot) return false | ||
53 | if (this.hasAccountAvatar && this.hasAccountDescription) return false | ||
54 | |||
55 | this.show() | ||
56 | } | ||
57 | ) | ||
58 | } | ||
59 | |||
60 | show () { | ||
61 | if (this.ref) return false | ||
62 | |||
63 | this.ref = this.modalService.open(this.modal, { | ||
64 | centered: true, | ||
65 | backdrop: 'static', | ||
66 | keyboard: false, | ||
67 | size: 'md' | ||
68 | }) | ||
69 | } | ||
70 | } | ||
diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts index 70d672306..a4c62c234 100644 --- a/client/src/app/shared/shared-icons/global-icon.component.ts +++ b/client/src/app/shared/shared-icons/global-icon.component.ts | |||
@@ -16,6 +16,7 @@ const icons = { | |||
16 | 'playlist-add': require('!!raw-loader?!../../../assets/images/misc/playlist-add.svg').default, // material ui | 16 | 'playlist-add': require('!!raw-loader?!../../../assets/images/misc/playlist-add.svg').default, // material ui |
17 | follower: require('!!raw-loader?!../../../assets/images/misc/account-arrow-left.svg').default, // material ui | 17 | follower: require('!!raw-loader?!../../../assets/images/misc/account-arrow-left.svg').default, // material ui |
18 | following: require('!!raw-loader?!../../../assets/images/misc/account-arrow-right.svg').default, // material ui | 18 | following: require('!!raw-loader?!../../../assets/images/misc/account-arrow-right.svg').default, // material ui |
19 | tip: require('!!raw-loader?!../../../assets/images/misc/tip.svg').default, // material ui | ||
19 | flame: require('!!raw-loader?!../../../assets/images/misc/flame.svg').default, | 20 | flame: require('!!raw-loader?!../../../assets/images/misc/flame.svg').default, |
20 | local: require('!!raw-loader?!../../../assets/images/misc/local.svg').default, | 21 | local: require('!!raw-loader?!../../../assets/images/misc/local.svg').default, |
21 | 22 | ||
diff --git a/client/src/app/shared/shared-main/misc/channels-setup-message.component.html b/client/src/app/shared/shared-main/misc/channels-setup-message.component.html new file mode 100644 index 000000000..3f94fa04c --- /dev/null +++ b/client/src/app/shared/shared-main/misc/channels-setup-message.component.html | |||
@@ -0,0 +1,8 @@ | |||
1 | <div *ngIf="hasChannelNotConfigured" class="channels-setup-message alert alert-info"> | ||
2 | <my-global-icon iconName="tip"></my-global-icon> | ||
3 | |||
4 | <div> | ||
5 | <div i18n>Some of your channels are not fully set up. Make them welcoming and explicit about what you publish by adding a <strong>banner</strong>, an <strong>avatar</strong> and a <strong>description</strong>.</div> | ||
6 | <a *ngIf="!hideLink" class="channels-settings-link" routerLink="/my-library/video-channels" i18n>Set up my channels</a> | ||
7 | </div> | ||
8 | </div> | ||
diff --git a/client/src/app/shared/shared-main/misc/channels-setup-message.component.scss b/client/src/app/shared/shared-main/misc/channels-setup-message.component.scss new file mode 100644 index 000000000..7dcba2ca5 --- /dev/null +++ b/client/src/app/shared/shared-main/misc/channels-setup-message.component.scss | |||
@@ -0,0 +1,32 @@ | |||
1 | @use '_variables' as *; | ||
2 | @use '_mixins' as *; | ||
3 | |||
4 | .channels-setup-message { | ||
5 | display: flex; | ||
6 | align-items: center; | ||
7 | justify-content: center; | ||
8 | |||
9 | my-global-icon { | ||
10 | width: 32px; | ||
11 | align-self: flex-start; | ||
12 | |||
13 | ::ng-deep { | ||
14 | svg { | ||
15 | fill: #0c5460; | ||
16 | } | ||
17 | } | ||
18 | |||
19 | + div { | ||
20 | margin-left: 10px; | ||
21 | text-align: center; | ||
22 | |||
23 | a.channels-settings-link { | ||
24 | @include peertube-button-link; | ||
25 | @include grey-button; | ||
26 | |||
27 | height: fit-content; | ||
28 | margin-top: 10px; | ||
29 | } | ||
30 | } | ||
31 | } | ||
32 | } | ||
diff --git a/client/src/app/shared/shared-main/misc/channels-setup-message.component.ts b/client/src/app/shared/shared-main/misc/channels-setup-message.component.ts new file mode 100644 index 000000000..50b98841a --- /dev/null +++ b/client/src/app/shared/shared-main/misc/channels-setup-message.component.ts | |||
@@ -0,0 +1,32 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { AuthService, User } from '@app/core' | ||
3 | import { VideoChannel } from '@app/shared/shared-main' | ||
4 | |||
5 | @Component({ | ||
6 | selector: 'my-channels-setup-message', | ||
7 | templateUrl: './channels-setup-message.component.html', | ||
8 | styleUrls: [ './channels-setup-message.component.scss' ] | ||
9 | }) | ||
10 | export class ChannelsSetupMessageComponent implements OnInit { | ||
11 | @Input() hideLink = false | ||
12 | |||
13 | user: User = null | ||
14 | |||
15 | constructor ( | ||
16 | private authService: AuthService | ||
17 | ) {} | ||
18 | |||
19 | get userInformationLoaded () { | ||
20 | return this.authService.userInformationLoaded | ||
21 | } | ||
22 | |||
23 | get hasChannelNotConfigured () { | ||
24 | return this.user.videoChannels | ||
25 | .filter((channel: VideoChannel) => (!channel.avatar || !channel.description)) | ||
26 | .length > 0 | ||
27 | } | ||
28 | |||
29 | ngOnInit () { | ||
30 | this.user = this.authService.getUser() | ||
31 | } | ||
32 | } | ||
diff --git a/client/src/app/shared/shared-main/misc/index.ts b/client/src/app/shared/shared-main/misc/index.ts index dc8ef9754..24134d7cd 100644 --- a/client/src/app/shared/shared-main/misc/index.ts +++ b/client/src/app/shared/shared-main/misc/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './channels-setup-message.component' | ||
1 | export * from './help.component' | 2 | export * from './help.component' |
2 | export * from './list-overflow.component' | 3 | export * from './list-overflow.component' |
3 | export * from './top-menu-dropdown.component' | 4 | export * from './top-menu-dropdown.component' |
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts index e5dfc59b2..80d0a84f3 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts | |||
@@ -34,7 +34,13 @@ import { CustomPageService } from './custom-page' | |||
34 | import { DateToggleComponent } from './date' | 34 | import { DateToggleComponent } from './date' |
35 | import { FeedComponent } from './feeds' | 35 | import { FeedComponent } from './feeds' |
36 | import { LoaderComponent, SmallLoaderComponent } from './loaders' | 36 | import { LoaderComponent, SmallLoaderComponent } from './loaders' |
37 | import { HelpComponent, ListOverflowComponent, SimpleSearchInputComponent, TopMenuDropdownComponent } from './misc' | 37 | import { |
38 | ChannelsSetupMessageComponent, | ||
39 | HelpComponent, | ||
40 | ListOverflowComponent, | ||
41 | SimpleSearchInputComponent, | ||
42 | TopMenuDropdownComponent | ||
43 | } from './misc' | ||
38 | import { PluginPlaceholderComponent } from './plugins' | 44 | import { PluginPlaceholderComponent } from './plugins' |
39 | import { ActorRedirectGuard } from './router' | 45 | import { ActorRedirectGuard } from './router' |
40 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' | 46 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' |
@@ -91,6 +97,7 @@ import { VideoChannelService } from './video-channel' | |||
91 | LoaderComponent, | 97 | LoaderComponent, |
92 | SmallLoaderComponent, | 98 | SmallLoaderComponent, |
93 | 99 | ||
100 | ChannelsSetupMessageComponent, | ||
94 | HelpComponent, | 101 | HelpComponent, |
95 | ListOverflowComponent, | 102 | ListOverflowComponent, |
96 | TopMenuDropdownComponent, | 103 | TopMenuDropdownComponent, |
@@ -146,6 +153,7 @@ import { VideoChannelService } from './video-channel' | |||
146 | LoaderComponent, | 153 | LoaderComponent, |
147 | SmallLoaderComponent, | 154 | SmallLoaderComponent, |
148 | 155 | ||
156 | ChannelsSetupMessageComponent, | ||
149 | HelpComponent, | 157 | HelpComponent, |
150 | ListOverflowComponent, | 158 | ListOverflowComponent, |
151 | TopMenuDropdownComponent, | 159 | TopMenuDropdownComponent, |
diff --git a/client/src/assets/images/misc/tip.svg b/client/src/assets/images/misc/tip.svg new file mode 100644 index 000000000..4b7d7543c --- /dev/null +++ b/client/src/assets/images/misc/tip.svg | |||
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><path d="M0,0h24v24H0V0z" fill="none"/></g><g><g><rect height="3" width="2" x="11" y="19"/><rect height="2" width="3" x="2" y="11"/><rect height="2" width="3" x="19" y="11"/><rect height="3" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -7.6665 17.8014)" width="1.99" x="16.66" y="16.66"/><rect height="1.99" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -10.9791 9.8041)" width="3" x="4.85" y="17.16"/><path d="M15,8.02V3H9v5.02C7.79,8.94,7,10.37,7,12c0,2.76,2.24,5,5,5s5-2.24,5-5C17,10.37,16.21,8.94,15,8.02z M11,5h2v2.1 C12.68,7.04,12.34,7,12,7s-0.68,0.04-1,0.1V5z"/></g></g></svg> | |||