forked from
me.webbeef.org/browser.html
Rewild Your Web
1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3import {
4 LitElement,
5 html,
6 css,
7} from "beaver://shared/third_party/lit/lit-all.min.js";
8
9class ContentBlockerPanel extends LitElement {
10 static properties = {
11 open: { type: Boolean, reflect: true },
12 origin: { type: String },
13 enabled: { type: Boolean },
14 blockedCount: { type: Number },
15 allowedCount: { type: Number },
16 x: { type: Number },
17 y: { type: Number },
18 };
19
20 static styles = css`
21 :host {
22 display: none;
23 }
24
25 :host([open]) {
26 display: block;
27 position: absolute;
28 inset: 0;
29 z-index: 10000;
30 }
31
32 .backdrop {
33 position: fixed;
34 inset: 0;
35 z-index: -1;
36 }
37
38 .panel {
39 position: absolute;
40 background: var(--bg-menu);
41 border: 1px solid var(--color-border);
42 border-radius: var(--radius-md);
43 padding: var(--spacing-md);
44 min-width: 220px;
45 box-shadow: 0 4px 16px var(--color-shadow-menu);
46 font-family: var(--font-family-base);
47 display: flex;
48 flex-direction: column;
49 gap: var(--spacing-sm);
50 animation: panel-in 0.18s cubic-bezier(0.16, 1, 0.3, 1);
51 }
52
53 @keyframes panel-in {
54 from {
55 opacity: 0;
56 transform: translateY(-4px);
57 }
58 }
59
60 .origin {
61 font-size: var(--font-size-sm);
62 color: var(--color-text-secondary);
63 white-space: nowrap;
64 overflow: hidden;
65 text-overflow: ellipsis;
66 }
67
68 .toggle-row {
69 display: flex;
70 align-items: center;
71 justify-content: space-between;
72 gap: var(--spacing-md);
73 }
74
75 .toggle-label {
76 font-size: var(--font-size-sm);
77 font-weight: var(--font-weight-bold);
78 }
79
80 .toggle-switch {
81 position: relative;
82 width: 2.4em;
83 height: 1.4em;
84 flex-shrink: 0;
85 }
86
87 .toggle-switch input {
88 opacity: 0;
89 width: 0;
90 height: 0;
91 }
92
93 .toggle-slider {
94 position: absolute;
95 inset: 0;
96 background: var(--color-border);
97 border-radius: 0.7em;
98 cursor: pointer;
99 transition: background var(--transition-fast);
100 }
101
102 .toggle-slider::before {
103 content: "";
104 position: absolute;
105 width: 1em;
106 height: 1em;
107 left: 0.2em;
108 bottom: 0.2em;
109 background: var(--color-text-on-header);
110 border-radius: 50%;
111 transition: transform var(--transition-fast);
112 }
113
114 .toggle-switch input:checked + .toggle-slider {
115 background: var(--color-primary);
116 }
117
118 .toggle-switch input:checked + .toggle-slider::before {
119 transform: translateX(1em);
120 }
121
122 .toggle-switch input:focus-visible + .toggle-slider {
123 outline: 2px solid var(--color-focus-ring);
124 outline-offset: 2px;
125 }
126
127 .stats {
128 font-size: var(--font-size-xs);
129 color: var(--color-text-tertiary);
130 }
131
132 .stats .count {
133 color: var(--color-text-secondary);
134 font-weight: var(--font-weight-bold);
135 }
136 `;
137
138 constructor() {
139 super();
140 this.open = false;
141 this.origin = "";
142 this.enabled = true;
143 this.blockedCount = 0;
144 this.allowedCount = 0;
145 this.x = 0;
146 this.y = 0;
147 }
148
149 async handleToggle(e) {
150 const newEnabled = e.target.checked;
151 this.enabled = newEnabled;
152 try {
153 await navigator.embedder.contentBlocker.setOriginEnabled(
154 this.origin,
155 newEnabled,
156 );
157 this.dispatchEvent(
158 new CustomEvent("blocker-toggled", {
159 bubbles: true,
160 composed: true,
161 detail: { enabled: newEnabled },
162 }),
163 );
164 } catch (err) {
165 console.error("[ContentBlockerPanel] setOriginEnabled failed:", err);
166 this.enabled = !newEnabled;
167 }
168 }
169
170 dismiss() {
171 this.open = false;
172 this.dispatchEvent(
173 new CustomEvent("panel-dismiss", { bubbles: true, composed: true }),
174 );
175 }
176
177 render() {
178 if (!this.open) return html``;
179
180 const total = this.blockedCount + this.allowedCount;
181 const percentage =
182 total > 0 ? Math.round((this.blockedCount / total) * 100) : 0;
183
184 let hostname = this.origin;
185 try {
186 hostname = new URL(this.origin).hostname;
187 } catch {}
188
189 const panelStyle = `right: calc(100% - ${this.x}px); top: ${this.y}px;`;
190
191 return html`
192 <div class="backdrop" @click=${this.dismiss}></div>
193 <div class="panel" style="${panelStyle}">
194 <div class="origin">${hostname}</div>
195 <div class="toggle-row">
196 <span class="toggle-label">Content blocking</span>
197 <label class="toggle-switch">
198 <input
199 type="checkbox"
200 .checked=${this.enabled}
201 @change=${this.handleToggle}
202 />
203 <span class="toggle-slider"></span>
204 </label>
205 </div>
206 <div class="stats">
207 <span class="count">${this.blockedCount}</span> blocked /
208 ${total} total (${percentage}%)
209 </div>
210 </div>
211 `;
212 }
213}
214
215customElements.define("content-blocker-panel", ContentBlockerPanel);