@@ -2,32 +2,16 @@ package com.csguard
22
33import android.app.AlertDialog
44import android.content.Context
5- import android.text.InputType
6- import android.view.ViewGroup
7- import android.widget.CheckBox
8- import android.widget.EditText
5+ import android.graphics.Color
6+ import android.graphics.Typeface
7+ import android.graphics.drawable.GradientDrawable
8+ import android.view.Gravity
99import android.widget.LinearLayout
10- import android.widget.RadioButton
11- import android.widget.RadioGroup
1210import android.widget.ScrollView
11+ import android.widget.Switch
1312import android.widget.TextView
14- import android.widget.Toast
15- import java.text.DateFormat
16- import java.util.Date
17-
18- /* *
19- * Settings UI for CSGuard.
20- *
21- * Three sections:
22- * 1. **Policy presets** — Strict / Strict-Verbose / Balanced / Permissive
23- * 2. **Allowlist management** — view/add/remove always-allow hosts
24- * 3. **Blocked log review** — see what CSGuard voided, with tap-to-approve
25- *
26- * The aggressive default is STRICT (silent void). The blocked-log section is
27- * the user's main tool for tuning: when something legit stops working (e.g.
28- * a CloudStream repo install URL got voided), they can find it in the log
29- * and tap "Allow always" to whitelist the host going forward.
30- */
13+ import com.lagradost.cloudstream3.APIHolder
14+
3115class GuardSettingsDialog (
3216 private val context : Context ,
3317 private val current : GuardPolicy ,
@@ -36,216 +20,139 @@ class GuardSettingsDialog(
3620 fun show () {
3721 val container = LinearLayout (context).apply {
3822 orientation = LinearLayout .VERTICAL
39- setPadding(48 , 32 , 48 , 32 )
23+ setPadding(48 , 48 , 48 , 48 )
24+ setBackgroundColor(Color .parseColor(" #121212" )) // Sleek Dark Mode
4025 }
4126
42- // ── Section 1: Policy preset ────────────────────────────────────
27+ // Title
4328 container.addView(TextView (context).apply {
44- text = " Protection Level"
45- textSize = 16f
29+ text = " 🛡 CSGuard Protection"
30+ textSize = 24f
31+ typeface = Typeface .DEFAULT_BOLD
32+ setTextColor(Color .WHITE )
4633 setPadding(0 , 0 , 0 , 16 )
4734 })
4835
49- val radioGroup = RadioGroup (context).apply { orientation = RadioGroup .VERTICAL }
50- val strictRb = RadioButton (context).apply {
51- id = 1
52- text = " Strict (silent) — block ALL external launches except allowlist\n [DEFAULT — the void is silent]"
53- isChecked = current == GuardPolicy .STRICT
54- }
55- val strictVerboseRb = RadioButton (context).apply {
56- id = 2
57- text = " Strict (verbose) — same as Strict, but show a toast on each block\n [for tuning your allowlist]"
58- isChecked = current == GuardPolicy .STRICT_VERBOSE
59- }
60- val balancedRb = RadioButton (context).apply {
61- id = 3
62- text = " Balanced — block known ad hosts + ad-pattern paths\n (allow other external launches)"
63- isChecked = current == GuardPolicy .DEFAULT && current != GuardPolicy .STRICT && current != GuardPolicy .STRICT_VERBOSE
64- }
65- val permissiveRb = RadioButton (context).apply {
66- id = 4
67- text = " Permissive — block only known ad-network hosts"
68- isChecked = current == GuardPolicy .PERMISSIVE
69- }
70- radioGroup.addView(strictRb)
71- radioGroup.addView(strictVerboseRb)
72- radioGroup.addView(balancedRb)
73- radioGroup.addView(permissiveRb)
74- container.addView(radioGroup)
75-
76- // ── Section 2: Allowlist management ─────────────────────────────
36+ // Subtitle
7737 container.addView(TextView (context).apply {
78- text = " \n Allowlist (hosts that bypass the block)"
79- textSize = 16f
80- setPadding(0 , 16 , 0 , 8 )
38+ text = " Select which providers are forbidden from launching external browser intents. CSGuard will silently void their ads."
39+ textSize = 14f
40+ setTextColor(Color .parseColor(" #B0B0B0" ))
41+ setPadding(0 , 0 , 0 , 32 )
8142 })
8243
83- val alwaysAllow = AllowlistStore .alwaysAllow().sorted()
84- val allowlistInfo = TextView (context).apply {
85- text = if (alwaysAllow.isEmpty()) {
86- " • (empty — relying on built-in SAFE_HOSTS baseline)"
87- } else {
88- alwaysAllow.joinToString(prefix = " • " , separator = " \n • " )
89- }
90- textSize = 12f
91- setPadding(16 , 0 , 16 , 8 )
92- }
93- container.addView(allowlistInfo)
94-
95- val addHostBtn = android.widget.Button (context).apply {
96- text = " + Add host to allowlist"
97- setOnClickListener { showAddHostDialog(context, allowlistInfo) }
98- }
99- container.addView(addHostBtn)
44+ var isGlobalStrict = current.blockAllUnknown
10045
101- val removeHostBtn = android.widget. Button (context). apply {
102- text = " − Remove host from allowlist "
103- setOnClickListener { showRemoveHostDialog(context, allowlistInfo) }
46+ // Master Toggle: Global Strict Mode
47+ val masterRow = createRow( " Global Strict Mode " , " Block ALL unknown external intents across the entire app. " , isGlobalStrict) { checked ->
48+ isGlobalStrict = checked
10449 }
105- container.addView(removeHostBtn )
50+ container.addView(masterRow )
10651
107- // ── Section 3: Blocked log review ───────────────────────────────
52+ // Section Title
10853 container.addView(TextView (context).apply {
109- text = " \n Recent Blocks (tap to approve)"
110- textSize = 16f
111- setPadding(0 , 16 , 0 , 8 )
54+ text = " Per-Provider Blocks"
55+ textSize = 18f
56+ typeface = Typeface .DEFAULT_BOLD
57+ setTextColor(Color .parseColor(" #E0E0E0" ))
58+ setPadding(0 , 32 , 0 , 16 )
11259 })
11360
114- val blockedLog = AllowlistStore .blockedLog()
115- val logInfo = TextView (context).apply {
116- text = if (blockedLog.isEmpty()) {
117- " • (no blocks recorded yet — try playing a video)"
118- } else {
119- val df = DateFormat .getDateTimeInstance(DateFormat .SHORT , DateFormat .MEDIUM )
120- buildString {
121- blockedLog.take(10 ).forEach { entry ->
122- val host = try { android.net.Uri .parse(entry.url).host ? : " ?" } catch (_: Throwable ) { " ?" }
123- append(" • [${df.format(Date (entry.timestamp))} ]\n " )
124- append(" host: $host \n " )
125- append(" caller: ${entry.caller} \n " )
126- append(" url: ${entry.url.take(80 )}${if (entry.url.length > 80 ) " ..." else " " } \n\n " )
127- }
128- if (blockedLog.size > 10 ) {
129- append(" … and ${blockedLog.size - 10 } more (showing 10 most recent)" )
130- }
61+ val blockedSet = AllowlistStore .blockedProviders().toMutableSet()
62+
63+ // Fetch all providers
64+ val providers = try {
65+ APIHolder .allProviders.sortedBy { it.name }
66+ } catch (_: Throwable ) { emptyList() }
67+
68+ if (providers.isEmpty()) {
69+ container.addView(TextView (context).apply {
70+ text = " No providers loaded yet."
71+ setTextColor(Color .RED )
72+ })
73+ } else {
74+ providers.forEach { provider ->
75+ val row = createRow(provider.name, " Plugin: ${provider.javaClass.simpleName} " , blockedSet.contains(provider.name)) { checked ->
76+ if (checked) blockedSet.add(provider.name) else blockedSet.remove(provider.name)
13177 }
78+ container.addView(row)
13279 }
133- textSize = 11f
134- setPadding(16 , 0 , 16 , 8 )
13580 }
136- container.addView(logInfo)
13781
138- val approveFromLogBtn = android.widget.Button (context).apply {
139- text = " ✓ Approve host from blocked log"
140- setOnClickListener { showApproveFromLogDialog(context, allowlistInfo) }
82+ val scroll = ScrollView (context).apply {
83+ addView(container)
14184 }
142- container.addView(approveFromLogBtn)
143-
144- val clearLogBtn = android.widget.Button (context).apply {
145- text = " 🗑 Clear blocked log"
146- setOnClickListener {
147- AllowlistStore .clearBlockedLog()
148- logInfo.text = " • (log cleared)"
149- Toast .makeText(context, " Blocked log cleared" , Toast .LENGTH_SHORT ).show()
150- }
151- }
152- container.addView(clearLogBtn)
153-
154- // ── Wrap in scroll view (might be tall) ─────────────────────────
155- val scroll = ScrollView (context).apply { addView(container) }
15685
157- AlertDialog .Builder (context)
158- .setTitle(" CSGuard Settings" )
86+ AlertDialog .Builder (context, android.R .style.Theme_DeviceDefault_Dialog_Alert )
15987 .setView(scroll)
160- .setPositiveButton(" Apply" ) { _, _ ->
161- val newPolicy = when (radioGroup.checkedRadioButtonId) {
162- 1 -> GuardPolicy .STRICT
163- 2 -> GuardPolicy .STRICT_VERBOSE
164- 3 -> GuardPolicy .DEFAULT
165- 4 -> GuardPolicy .PERMISSIVE
166- else -> GuardPolicy .STRICT
167- }
88+ .setPositiveButton(" Save Settings" ) { _, _ ->
89+ // Apply blocked providers
90+ val currentBlocks = AllowlistStore .blockedProviders()
91+ currentBlocks.forEach { AllowlistStore .removeBlockedProvider(it) }
92+ blockedSet.forEach { AllowlistStore .addBlockedProvider(it) }
93+
94+ // Create new policy
95+ val newPolicy = GuardPolicy (
96+ blockKnownAdHosts = true ,
97+ blockAdPaths = true ,
98+ blockAllUnknown = isGlobalStrict,
99+ showToast = current.showToast
100+ )
168101 onApply(newPolicy)
169- Toast .makeText(context, " CSGuard: $newPolicy " , Toast .LENGTH_SHORT ).show()
170102 }
171103 .setNegativeButton(" Cancel" , null )
172104 .show()
173105 }
174106
175- private fun showAddHostDialog (ctx : Context , allowlistInfo : TextView ) {
176- val input = EditText (ctx).apply {
177- hint = " e.g. example.com (no scheme, no path)"
178- inputType = InputType .TYPE_CLASS_TEXT or InputType .TYPE_TEXT_VARIATION_URI
107+ private fun createRow (title : String , subtitle : String , isChecked : Boolean , onToggle : (Boolean ) -> Unit ): LinearLayout {
108+ val row = LinearLayout (context).apply {
109+ orientation = LinearLayout .HORIZONTAL
110+ gravity = Gravity .CENTER_VERTICAL
111+ setPadding(32 , 32 , 32 , 32 )
112+
113+ val shape = GradientDrawable ()
114+ shape.shape = GradientDrawable .RECTANGLE
115+ shape.cornerRadius = 24f
116+ shape.setColor(Color .parseColor(" #1E1E1E" ))
117+ background = shape
118+
119+ val params = LinearLayout .LayoutParams (
120+ LinearLayout .LayoutParams .MATCH_PARENT ,
121+ LinearLayout .LayoutParams .WRAP_CONTENT
122+ )
123+ params.setMargins(0 , 0 , 0 , 16 )
124+ layoutParams = params
179125 }
180- AlertDialog .Builder (ctx)
181- .setTitle(" Add host to allowlist" )
182- .setView(input)
183- .setPositiveButton(" Add" ) { _, _ ->
184- val host = input.text.toString().trim()
185- if (host.isNotEmpty()) {
186- AllowlistStore .addAlwaysAllow(host)
187- refreshAllowlistView(allowlistInfo)
188- Toast .makeText(ctx, " Added: $host " , Toast .LENGTH_SHORT ).show()
189- }
190- }
191- .setNegativeButton(" Cancel" , null )
192- .show()
193- }
194126
195- private fun showRemoveHostDialog (ctx : Context , allowlistInfo : TextView ) {
196- val hosts = AllowlistStore .alwaysAllow().sorted()
197- if (hosts.isEmpty()) {
198- Toast .makeText(ctx, " Allowlist is empty" , Toast .LENGTH_SHORT ).show()
199- return
127+ val textLayout = LinearLayout (context).apply {
128+ orientation = LinearLayout .VERTICAL
129+ layoutParams = LinearLayout .LayoutParams (0 , LinearLayout .LayoutParams .WRAP_CONTENT , 1f )
200130 }
201- val items = hosts.toTypedArray()
202- val checked = BooleanArray (items.size) { false }
203- AlertDialog .Builder (ctx)
204- .setTitle(" Remove hosts from allowlist" )
205- .setMultiChoiceItems(items, checked) { _, which, isChecked ->
206- checked[which] = isChecked
207- }
208- .setPositiveButton(" Remove selected" ) { _, _ ->
209- items.indices.filter { checked[it] }.forEach { AllowlistStore .removeAlwaysAllow(items[it]) }
210- refreshAllowlistView(allowlistInfo)
211- }
212- .setNegativeButton(" Cancel" , null )
213- .show()
214- }
215131
216- private fun showApproveFromLogDialog (ctx : Context , allowlistInfo : TextView ) {
217- val log = AllowlistStore .blockedLog()
218- if (log.isEmpty()) {
219- Toast .makeText(ctx, " Blocked log is empty" , Toast .LENGTH_SHORT ).show()
220- return
132+ val titleView = TextView (context).apply {
133+ text = title
134+ textSize = 16f
135+ setTextColor(Color .WHITE )
136+ typeface = Typeface .DEFAULT_BOLD
137+ }
138+
139+ val subtitleView = TextView (context).apply {
140+ text = subtitle
141+ textSize = 12f
142+ setTextColor(Color .parseColor(" #888888" ))
143+ setPadding(0 , 4 , 0 , 0 )
221144 }
222- // Group by host, show distinct hosts
223- val hosts = log.mapNotNull { entry ->
224- try { android.net.Uri .parse(entry.url).host } catch (_: Throwable ) { null }
225- }.distinct()
226-
227- val items = hosts.toTypedArray()
228- val checked = BooleanArray (items.size) { false }
229- AlertDialog .Builder (ctx)
230- .setTitle(" Approve hosts from blocked log" )
231- .setMultiChoiceItems(items, checked) { _, which, isChecked ->
232- checked[which] = isChecked
233- }
234- .setPositiveButton(" Add to allowlist" ) { _, _ ->
235- items.indices.filter { checked[it] }.forEach { AllowlistStore .addAlwaysAllow(items[it]) }
236- refreshAllowlistView(allowlistInfo)
237- Toast .makeText(ctx, " Added ${checked.count { it }} hosts" , Toast .LENGTH_SHORT ).show()
238- }
239- .setNegativeButton(" Cancel" , null )
240- .show()
241- }
242145
243- private fun refreshAllowlistView ( view : TextView ) {
244- val alwaysAllow = AllowlistStore .alwaysAllow().sorted( )
245- view.text = if (alwaysAllow.isEmpty()) {
246- " • (empty — relying on built-in SAFE_HOSTS baseline) "
247- } else {
248- alwaysAllow.joinToString(prefix = " • " , separator = " \n • " )
146+ textLayout.addView(titleView)
147+ textLayout.addView(subtitleView )
148+
149+ val toggle = Switch (context). apply {
150+ this .isChecked = isChecked
151+ setOnCheckedChangeListener { _, checked -> onToggle(checked) }
249152 }
153+
154+ row.addView(textLayout)
155+ row.addView(toggle)
156+ return row
250157 }
251158}
0 commit comments