diff --git a/README.md b/README.md
index 62c2721..77d169c 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,21 @@
A professional, high-performance **Short URL Manager** plugin for [Filament v5](https://filamentphp.com). Built from scratch with cutting-edge practices, proxy resistance, offline Geo-IP engines, enterprise-grade smart targeting, and zero external shortening API dependencies.
+## Screenshots
+
+
+
+
+
---
## Features
@@ -26,7 +41,7 @@ A professional, high-performance **Short URL Manager** plugin for [Filament v5](
- ⚡ **Ultra-Fast Redirects** — Redirections resolve in milliseconds. Analytical tasks, event dispatching, and GA4 payloads are processed asynchronously via Laravel Queue jobs.
- 🎯 **Google Analytics 4 server-side tracking** — Native integration with the GA4 Measurement Protocol to bypass client-side AdBlockers completely.
- ⚙️ **Dual-way UTM Campaign Builder** — Built-in form builder synchronizes UTM parameters with your destination URLs in real-time (two-way binding).
-- 🔒 **Link Rules & Expiry** — Disable links manually, set expiration dates, or activate single-use links that deactivate automatically after the first click.
+- 🔒 **Link Validity Ranges & Expiry** — Set activation date ranges (From - To), custom visit limit counters (e.g. active for 3 clicks then expires), single-use restrictions, and custom fallback redirect URLs on expiration instead of static 410 Gone errors.
- ➡️ **Query Parameter Forwarding** — Dynamically forward client query parameters (e.g. ad tokens, discount codes) to the destination URL.
- 🛠️ **Dedicated Settings GUI** — Manage global configuration (routing, Geo-IP, GA4, cache, rate limiting, aggregation) directly inside the Filament panel without modifying files or `.env` files.
- 💻 **Fluent Developer Builder** — Native model query builder pattern and robust programmatic generation APIs.
@@ -436,10 +451,14 @@ echo $shortUrl->getShortUrl(); // https://yourdomain.com/s/promo2026
| `notes()` | `string` | Appends internal notes for administrators. |
| `singleUse()` | `bool` (default `true`) | Makes the short URL expire immediately after the first successful click. |
| `forwardQueryParams()` | `bool` (default `true`) | Forwards client-side incoming parameters (e.g. `?gclid=xxx`) to the final target. |
-| `expiresAt()` | `DateTimeInterface\|Carbon\|null` | Automatically sets a timestamp after which the URL returns a `410 Gone` error. |
+| `expiresAt()` | `DateTimeInterface\|Carbon\|null` | Automatically sets an expiration timestamp after which the URL is inactive. |
+| `activatedAt()` | `DateTimeInterface\|Carbon\|null` | Sets the timestamp from which the URL is active. |
+| `deactivatedAt()` | `DateTimeInterface\|Carbon\|null` | Sets the timestamp after which the URL is inactive. |
+| `maxVisits()` | `int\|null` | Sets a custom limit of total visits allowed. |
+| `expirationRedirectUrl()` | `string\|null` | Sets a custom URL to redirect to when expired/inactive. |
| `trackVisits()` | `bool` (default `true`) | Toggles statistical logging for this link. |
| `withTracing()` | `array` | Appends non-empty tracking parameters (like UTM codes) directly to the target URL. |
-| `create()` | `void` | Persists the model to the database and returns the `ShortUrl` instance. |
+| `create()` | `ShortUrl` | Persists the model to the database and returns the `ShortUrl` instance. |
---
@@ -501,7 +520,15 @@ All migrations are compatible with **SQLite**, **MySQL**, and **PostgreSQL**:
## Changelog
-### v1.3.0 (Latest)
+### v1.4.0 (Latest)
+- **Validity Date Ranges (From-To)** — Set activation dates (`activated_at` and `expires_at`) to control exactly when a short link is active.
+- **Custom Visit Limit Counters** — Define a custom maximum visit limit (`max_visits`) after which a link automatically expires (e.g., active for 3 hits).
+- **Custom Expiration Fallbacks** — Redirect expired/inactive visitors to a custom `expiration_redirect_url` rather than showing a static 410 Gone error page.
+- **Reactive Validity Controls** — Master switch to toggle date limits, including bidirectional datetime picker constraints (Active From cannot exceed Expires At and vice versa) and custom UI field visibility.
+- **Smart Model Observers** — Automatic cleanup of unused parameters (like clearing `max_visits` if `single_use` is enabled, and clearing expiration fallbacks if date limits are off) to guarantee database consistency.
+- **Fluent Builder APIs** — Fluent method additions (`activatedAt()`, `deactivatedAt()`, `maxVisits()`, `expirationRedirectUrl()`) in the developer query builder.
+
+### v1.3.0
- **Automatic Scheduler Registration** — Zero-configuration task registration within the ServiceProvider booted phase (dynamically honors Settings toggles).
- **Interactive Settings Validators** — Adds real-time "Test connection" verify action for GA4 Measurement Protocol and "Verify file" action for MaxMind database paths.
- **Robust Table Row Copy Action** — High-reliability, conflict-free click-to-copy in table rows with built-in fallback helper for non-HTTPS (secure context) browser environments.
diff --git a/art/1.png b/art/1.png
new file mode 100644
index 0000000..50ed1ca
Binary files /dev/null and b/art/1.png differ
diff --git a/art/2.png b/art/2.png
new file mode 100644
index 0000000..7ab3e38
Binary files /dev/null and b/art/2.png differ
diff --git a/art/3.png b/art/3.png
new file mode 100644
index 0000000..ccebce5
Binary files /dev/null and b/art/3.png differ
diff --git a/art/4.png b/art/4.png
new file mode 100644
index 0000000..3f16675
Binary files /dev/null and b/art/4.png differ
diff --git a/config/filament-short-url.php b/config/filament-short-url.php
index d34c75a..3be1103 100644
--- a/config/filament-short-url.php
+++ b/config/filament-short-url.php
@@ -93,6 +93,15 @@ return [
*/
'queue_connection' => env('SHORT_URL_QUEUE', 'sync'),
+ /*
+ |--------------------------------------------------------------------------
+ | Queue Name
+ |--------------------------------------------------------------------------
+ | The queue name to which visit tracking and counter jobs are dispatched.
+ |
+ */
+ 'queue_name' => env('SHORT_URL_QUEUE_NAME', 'default'),
+
/*
|--------------------------------------------------------------------------
| Redirect Cache TTL
@@ -127,8 +136,8 @@ return [
'foreground_color' => '#000000',
'background_color' => '#ffffff',
'gradient_enabled' => false,
- 'gradient_from' => '#000000',
- 'gradient_to' => '#ffffff',
+ 'gradient_from' => '#4f46e5',
+ 'gradient_to' => '#06b6d4',
'gradient_type' => 'linear',
],
diff --git a/database/migrations/2026_06_02_000006_add_max_visits_and_expiration_redirect_to_short_urls_table.php b/database/migrations/2026_06_02_000006_add_max_visits_and_expiration_redirect_to_short_urls_table.php
new file mode 100644
index 0000000..e3271f5
--- /dev/null
+++ b/database/migrations/2026_06_02_000006_add_max_visits_and_expiration_redirect_to_short_urls_table.php
@@ -0,0 +1,23 @@
+unsignedInteger('max_visits')->nullable();
+ $table->string('expiration_redirect_url', 2048)->nullable();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('short_urls', function (Blueprint $table): void {
+ $table->dropColumn(['max_visits', 'expiration_redirect_url']);
+ });
+ }
+};
diff --git a/resources/lang/en/countries.php b/resources/lang/en/countries.php
new file mode 100644
index 0000000..464b662
--- /dev/null
+++ b/resources/lang/en/countries.php
@@ -0,0 +1,253 @@
+ 'Afghanistan',
+ 'AX' => 'Åland Islands',
+ 'AL' => 'Albania',
+ 'DZ' => 'Algeria',
+ 'AS' => 'American Samoa',
+ 'AD' => 'Andorra',
+ 'AO' => 'Angola',
+ 'AI' => 'Anguilla',
+ 'AQ' => 'Antarctica',
+ 'AG' => 'Antigua and Barbuda',
+ 'AR' => 'Argentina',
+ 'AM' => 'Armenia',
+ 'AW' => 'Aruba',
+ 'AU' => 'Australia',
+ 'AT' => 'Austria',
+ 'AZ' => 'Azerbaijan',
+ 'BS' => 'Bahamas',
+ 'BH' => 'Bahrain',
+ 'BD' => 'Bangladesh',
+ 'BB' => 'Barbados',
+ 'BY' => 'Belarus',
+ 'BE' => 'Belgium',
+ 'BZ' => 'Belize',
+ 'BJ' => 'Benin',
+ 'BM' => 'Bermuda',
+ 'BT' => 'Bhutan',
+ 'BO' => 'Bolivia',
+ 'BQ' => 'Bonaire, Sint Eustatius and Saba',
+ 'BA' => 'Bosnia and Herzegovina',
+ 'BW' => 'Botswana',
+ 'BV' => 'Bouvet Island',
+ 'BR' => 'Brazil',
+ 'IO' => 'British Indian Ocean Territory',
+ 'BN' => 'Brunei Darussalam',
+ 'BG' => 'Bulgaria',
+ 'BF' => 'Burkina Faso',
+ 'BI' => 'Burundi',
+ 'CV' => 'Cabo Verde',
+ 'KH' => 'Cambodia',
+ 'CM' => 'Cameroon',
+ 'CA' => 'Canada',
+ 'KY' => 'Cayman Islands',
+ 'CF' => 'Central African Republic',
+ 'TD' => 'Chad',
+ 'CL' => 'Chile',
+ 'CN' => 'China',
+ 'CX' => 'Christmas Island',
+ 'CC' => 'Cocos (Keeling) Islands',
+ 'CO' => 'Colombia',
+ 'KM' => 'Comoros',
+ 'CD' => 'Congo (Democratic Republic)',
+ 'CG' => 'Congo (Republic)',
+ 'CK' => 'Cook Islands',
+ 'CR' => 'Costa Rica',
+ 'CI' => 'Côte d\'Ivoire',
+ 'HR' => 'Croatia',
+ 'CU' => 'Cuba',
+ 'CW' => 'Curaçao',
+ 'CY' => 'Cyprus',
+ 'CZ' => 'Czech Republic',
+ 'DK' => 'Denmark',
+ 'DJ' => 'Djibouti',
+ 'DM' => 'Dominica',
+ 'DO' => 'Dominican Republic',
+ 'EC' => 'Ecuador',
+ 'EG' => 'Egypt',
+ 'SV' => 'El Salvador',
+ 'GQ' => 'Equatorial Guinea',
+ 'ER' => 'Eritrea',
+ 'EE' => 'Estonia',
+ 'SZ' => 'Eswatini',
+ 'ET' => 'Ethiopia',
+ 'FK' => 'Falkland Islands',
+ 'FO' => 'Faroe Islands',
+ 'FJ' => 'Fiji',
+ 'FI' => 'Finland',
+ 'FR' => 'France',
+ 'GF' => 'French Guiana',
+ 'PF' => 'French Polynesia',
+ 'TF' => 'French Southern Territories',
+ 'GA' => 'Gabon',
+ 'GM' => 'Gambia',
+ 'GE' => 'Georgia',
+ 'DE' => 'Germany',
+ 'GH' => 'Ghana',
+ 'GI' => 'Gibraltar',
+ 'GR' => 'Greece',
+ 'GL' => 'Greenland',
+ 'GD' => 'Grenada',
+ 'GP' => 'Guadeloupe',
+ 'GU' => 'Guam',
+ 'GT' => 'Guatemala',
+ 'GG' => 'Guernsey',
+ 'GN' => 'Guinea',
+ 'GW' => 'Guinea-Bissau',
+ 'GY' => 'Guyana',
+ 'HT' => 'Haiti',
+ 'HM' => 'Heard Island and McDonald Islands',
+ 'VA' => 'Holy See',
+ 'HN' => 'Honduras',
+ 'HK' => 'Hong Kong',
+ 'HU' => 'Hungary',
+ 'IS' => 'Iceland',
+ 'IN' => 'India',
+ 'ID' => 'Indonesia',
+ 'IR' => 'Iran',
+ 'IQ' => 'Iraq',
+ 'IE' => 'Ireland',
+ 'IM' => 'Isle of Man',
+ 'IL' => 'Israel',
+ 'IT' => 'Italy',
+ 'JM' => 'Jamaica',
+ 'JP' => 'Japan',
+ 'JE' => 'Jersey',
+ 'JO' => 'Jordan',
+ 'KZ' => 'Kazakhstan',
+ 'KE' => 'Kenya',
+ 'KI' => 'Kiribati',
+ 'KP' => 'North Korea',
+ 'KR' => 'South Korea',
+ 'KW' => 'Kuwait',
+ 'KG' => 'Kyrgyzstan',
+ 'LA' => 'Lao People\'s Democratic Republic',
+ 'LV' => 'Latvia',
+ 'LB' => 'Lebanon',
+ 'LS' => 'Lesotho',
+ 'LR' => 'Liberia',
+ 'LY' => 'Libya',
+ 'LI' => 'Liechtenstein',
+ 'LT' => 'Lithuania',
+ 'LU' => 'Luxembourg',
+ 'MO' => 'Macao',
+ 'MG' => 'Madagascar',
+ 'MW' => 'Malawi',
+ 'MY' => 'Malaysia',
+ 'MV' => 'Maldives',
+ 'ML' => 'Mali',
+ 'MT' => 'Malta',
+ 'MH' => 'Marshall Islands',
+ 'MQ' => 'Martinique',
+ 'MR' => 'Mauritania',
+ 'MU' => 'Mauritius',
+ 'YT' => 'Mayotte',
+ 'MX' => 'Mexico',
+ 'FM' => 'Micronesia',
+ 'MD' => 'Moldova',
+ 'MC' => 'Monaco',
+ 'MN' => 'Mongolia',
+ 'ME' => 'Montenegro',
+ 'MS' => 'Montserrat',
+ 'MA' => 'Morocco',
+ 'MZ' => 'Mozambique',
+ 'MM' => 'Myanmar',
+ 'NA' => 'Namibia',
+ 'NR' => 'Nauru',
+ 'NP' => 'Nepal',
+ 'NL' => 'Netherlands',
+ 'NC' => 'New Caledonia',
+ 'NZ' => 'New Zealand',
+ 'NI' => 'Nicaragua',
+ 'NE' => 'Niger',
+ 'NG' => 'Nigeria',
+ 'NU' => 'Niue',
+ 'NF' => 'Norfolk Island',
+ 'MK' => 'North Macedonia',
+ 'MP' => 'Northern Mariana Islands',
+ 'NO' => 'Norway',
+ 'OM' => 'Oman',
+ 'PK' => 'Pakistan',
+ 'PW' => 'Palau',
+ 'PS' => 'Palestine',
+ 'PA' => 'Panama',
+ 'PG' => 'Papua New Guinea',
+ 'PY' => 'Paraguay',
+ 'PE' => 'Peru',
+ 'PH' => 'Philippines',
+ 'PN' => 'Pitcairn',
+ 'PL' => 'Poland',
+ 'PT' => 'Portugal',
+ 'PR' => 'Puerto Rico',
+ 'QA' => 'Qatar',
+ 'RE' => 'Réunion',
+ 'RO' => 'Romania',
+ 'RU' => 'Russian Federation',
+ 'RW' => 'Rwanda',
+ 'BL' => 'Saint Barthélemy',
+ 'SH' => 'Saint Helena, Ascension and Tristan da Cunha',
+ 'KN' => 'Saint Kitts and Nevis',
+ 'LC' => 'Saint Lucia',
+ 'MF' => 'Saint Martin (French part)',
+ 'PM' => 'Saint Pierre and Miquelon',
+ 'VC' => 'Saint Vincent and the Grenadines',
+ 'WS' => 'Samoa',
+ 'SM' => 'San Marino',
+ 'ST' => 'Sao Tome and Principe',
+ 'SA' => 'Saudi Arabia',
+ 'SN' => 'Senegal',
+ 'RS' => 'Serbia',
+ 'SC' => 'Seychelles',
+ 'SL' => 'Sierra Leone',
+ 'SG' => 'Singapore',
+ 'SX' => 'Sint Maarten (Dutch part)',
+ 'SK' => 'Slovakia',
+ 'SI' => 'Slovenia',
+ 'SB' => 'Solomon Islands',
+ 'SO' => 'Somalia',
+ 'ZA' => 'South Africa',
+ 'GS' => 'South Georgia and the South Sandwich Islands',
+ 'SS' => 'South Sudan',
+ 'ES' => 'Spain',
+ 'LK' => 'Sri Lanka',
+ 'SD' => 'Sudan',
+ 'SR' => 'Suriname',
+ 'SJ' => 'Svalbard and Jan Mayen',
+ 'SE' => 'Sweden',
+ 'CH' => 'Switzerland',
+ 'SY' => 'Syrian Arab Republic',
+ 'TW' => 'Taiwan',
+ 'TJ' => 'Tajikistan',
+ 'TZ' => 'Tanzania',
+ 'TH' => 'Thailand',
+ 'TL' => 'Timor-Leste',
+ 'TG' => 'Togo',
+ 'TK' => 'Tokelau',
+ 'TO' => 'Tonga',
+ 'TT' => 'Trinidad and Tobago',
+ 'TN' => 'Tunisia',
+ 'TR' => 'Turkey',
+ 'TM' => 'Turkmenistan',
+ 'TC' => 'Turks and Caicos Islands',
+ 'TV' => 'Tuvalu',
+ 'UG' => 'Uganda',
+ 'UA' => 'Ukraine',
+ 'AE' => 'United Arab Emirates',
+ 'GB' => 'United Kingdom',
+ 'UM' => 'United States Minor Outlying Islands',
+ 'US' => 'United States',
+ 'UY' => 'Uruguay',
+ 'UZ' => 'Uzbekistan',
+ 'VU' => 'Vanuatu',
+ 'VE' => 'Venezuela',
+ 'VN' => 'Viet Nam',
+ 'VG' => 'Virgin Islands (British)',
+ 'VI' => 'Virgin Islands (U.S.)',
+ 'WF' => 'Wallis and Futuna',
+ 'EH' => 'Western Sahara',
+ 'YE' => 'Yemen',
+ 'ZM' => 'Zambia',
+ 'ZW' => 'Zimbabwe',
+];
diff --git a/resources/lang/en/default.php b/resources/lang/en/default.php
index 6f18605..7b98e6e 100644
--- a/resources/lang/en/default.php
+++ b/resources/lang/en/default.php
@@ -24,6 +24,13 @@ return [
'forward_query_params' => 'Forward Query Parameters',
'forward_query_params_helper' => 'Append incoming query parameters (like UTM tags) to the destination URL.',
'expires_at' => 'Expires At',
+ 'activated_at' => 'Active From',
+ 'max_visits' => 'Max Visits Limit',
+ 'max_visits_helper' => 'Optional limit of total visits allowed before this link is deactivated.',
+ 'expiration_redirect_url' => 'Expiration Redirect URL',
+ 'expiration_redirect_url_helper' => 'Fallback URL to redirect to when the link is expired or inactive (leaves blank for 410 Gone error page).',
+ 'form_section_validity' => 'Validity & Limits',
+ 'use_date_validity' => 'Set Date Range Limits',
'notes' => 'Internal Notes',
'ga_tracking_id' => 'Google Analytics 4 Measurement ID',
'ga_tracking_id_helper' => 'Optional G-XXXXXXXXXX ID to track redirects server-side.',
@@ -288,6 +295,39 @@ return [
'settings_rate_limiting_decay_seconds' => 'Decay Window (Seconds)',
'settings_rate_limiting_decay_seconds_helper' => 'Time period for the rate limiter.',
+ // Queue Settings Additions
+ 'settings_queue_name' => 'Queue Name',
+ 'settings_queue_name_helper' => 'The target queue to which tracking and buffering sync jobs are dispatched. Default: "default".',
+ 'settings_geoip_stats_cache_ttl' => 'Stats Dashboard Cache TTL',
+ 'settings_geoip_stats_cache_ttl_helper' => 'Number of seconds to cache query metrics on the dashboard statistics page (prevents database load). Default: 300 (5 minutes).',
+
+ // Default Tracking Tab
+ 'settings_tab_tracking_defaults' => 'Default Tracking',
+ 'settings_section_tracking_defaults' => 'Default Visitor Tracking Settings',
+ 'settings_section_tracking_defaults_helper' => 'These settings govern the default tracking status for newly created short URLs.',
+ 'settings_track_visits_default' => 'Enable Tracking by Default',
+ 'settings_track_ip_default' => 'Default IP Address Tracking',
+ 'settings_track_browser_default' => 'Default Browser Tracking',
+ 'settings_track_browser_version_default' => 'Default Browser Version Tracking',
+ 'settings_track_os_default' => 'Default OS Tracking',
+ 'settings_track_os_version_default' => 'Default OS Version Tracking',
+ 'settings_track_referer_default' => 'Default Referer URL Tracking',
+ 'settings_track_device_type_default' => 'Default Device Type Tracking',
+
+ // QR Defaults Tab
+ 'settings_tab_qr' => 'QR Defaults',
+ 'settings_section_qr_defaults' => 'Default QR Code Design Options',
+ 'settings_section_qr_defaults_helper' => 'Configure standard design styling automatically loaded for newly generated QR codes.',
+ 'settings_qr_size' => 'Default Size (px)',
+ 'settings_qr_margin' => 'Default Margin',
+ 'settings_qr_dot_style' => 'Default Dot Style',
+ 'settings_qr_foreground_color' => 'Default Foreground Color',
+ 'settings_qr_background_color' => 'Default Background Color',
+ 'settings_qr_gradient_enabled' => 'Enable Gradient by Default',
+ 'settings_qr_gradient_from' => 'Gradient Color From',
+ 'settings_qr_gradient_to' => 'Gradient Color To',
+ 'settings_qr_gradient_type' => 'Default Gradient Type',
+
// Views
'password_title' => 'Password Required',
'password_description' => 'This link is password-protected. Please enter the correct password to continue.',
diff --git a/resources/lang/pl/countries.php b/resources/lang/pl/countries.php
new file mode 100644
index 0000000..71a4d9c
--- /dev/null
+++ b/resources/lang/pl/countries.php
@@ -0,0 +1,253 @@
+ 'Afganistan',
+ 'AX' => 'Wyspy Alandzkie',
+ 'AL' => 'Albania',
+ 'DZ' => 'Algieria',
+ 'AS' => 'Samoa Amerykańskie',
+ 'AD' => 'Andora',
+ 'AO' => 'Angola',
+ 'AI' => 'Anguilla',
+ 'AQ' => 'Antarktyda',
+ 'AG' => 'Antigua i Barbuda',
+ 'AR' => 'Argentyna',
+ 'AM' => 'Armenia',
+ 'AW' => 'Aruba',
+ 'AU' => 'Australia',
+ 'AT' => 'Austria',
+ 'AZ' => 'Azerbejdżan',
+ 'BS' => 'Bahamy',
+ 'BH' => 'Bahrajn',
+ 'BD' => 'Bangladesz',
+ 'BB' => 'Barbados',
+ 'BY' => 'Białoruś',
+ 'BE' => 'Belgia',
+ 'BZ' => 'Belize',
+ 'BJ' => 'Benin',
+ 'BM' => 'Bermudy',
+ 'BT' => 'Bhutan',
+ 'BO' => 'Boliwia',
+ 'BQ' => 'Bonaire, Sint Eustatius i Saba',
+ 'BA' => 'Bośnia i Hercegowina',
+ 'BW' => 'Botswana',
+ 'BV' => 'Wyspa Bouveta',
+ 'BR' => 'Brazylia',
+ 'IO' => 'Brytyjskie Terytorium Oceanu Indyjskiego',
+ 'BN' => 'Brunei',
+ 'BG' => 'Bułgaria',
+ 'BF' => 'Burkina Faso',
+ 'BI' => 'Burundi',
+ 'CV' => 'Cabo Verde',
+ 'KH' => 'Kambodża',
+ 'CM' => 'Kamerun',
+ 'CA' => 'Kanada',
+ 'KY' => 'Kajmany',
+ 'CF' => 'Republika Środkowoafrykańska',
+ 'TD' => 'Czad',
+ 'CL' => 'Chile',
+ 'CN' => 'Chiny',
+ 'CX' => 'Wyspa Bożego Narodzenia',
+ 'CC' => 'Wyspy Kokosowe',
+ 'CO' => 'Kolumbia',
+ 'KM' => 'Komory',
+ 'CD' => 'Demokratyczna Republika Konga',
+ 'CG' => 'Kongo',
+ 'CK' => 'Wyspy Cooka',
+ 'CR' => 'Kostaryka',
+ 'CI' => 'Wybrzeże Kości Słoniowej',
+ 'HR' => 'Chorwacja',
+ 'CU' => 'Kuba',
+ 'CW' => 'Curaçao',
+ 'CY' => 'Cypr',
+ 'CZ' => 'Czechy',
+ 'DK' => 'Dania',
+ 'DJ' => 'Dżibuti',
+ 'DM' => 'Dominika',
+ 'DO' => 'Dominikana',
+ 'EC' => 'Ekwador',
+ 'EG' => 'Egipt',
+ 'SV' => 'Salwador',
+ 'GQ' => 'Gwinea Równikowa',
+ 'ER' => 'Erytrea',
+ 'EE' => 'Estonia',
+ 'SZ' => 'Eswatini',
+ 'ET' => 'Etiopia',
+ 'FK' => 'Falklandy',
+ 'FO' => 'Wyspy Owcze',
+ 'FJ' => 'Fidżi',
+ 'FI' => 'Finlandia',
+ 'FR' => 'Francja',
+ 'GF' => 'Gujana Francuska',
+ 'PF' => 'Polinezja Francuska',
+ 'TF' => 'Francuskie Terytoria Południowe',
+ 'GA' => 'Gabon',
+ 'GM' => 'Gambia',
+ 'GE' => 'Gruzja',
+ 'DE' => 'Niemcy',
+ 'GH' => 'Ghana',
+ 'GI' => 'Gibraltar',
+ 'GR' => 'Grecja',
+ 'GL' => 'Grenlandia',
+ 'GD' => 'Grenada',
+ 'GP' => 'Gwadelupa',
+ 'GU' => 'Guam',
+ 'GT' => 'Gwatemala',
+ 'GG' => 'Guernsey',
+ 'GN' => 'Gwinea',
+ 'GW' => 'Gwinea Bissau',
+ 'GY' => 'Gujana',
+ 'HT' => 'Haiti',
+ 'HM' => 'Wyspy Heard i McDonalda',
+ 'VA' => 'Watykan',
+ 'HN' => 'Honduras',
+ 'HK' => 'Hongkong',
+ 'HU' => 'Węgry',
+ 'IS' => 'Islandia',
+ 'IN' => 'Indie',
+ 'ID' => 'Indonezja',
+ 'IR' => 'Iran',
+ 'IQ' => 'Irak',
+ 'IE' => 'Irlandia',
+ 'IM' => 'Wyspa Man',
+ 'IL' => 'Izrael',
+ 'IT' => 'Włochy',
+ 'JM' => 'Jamajka',
+ 'JP' => 'Japonia',
+ 'JE' => 'Jersey',
+ 'JO' => 'Jordania',
+ 'KZ' => 'Kazachstan',
+ 'KE' => 'Kenia',
+ 'KI' => 'Kiribati',
+ 'KP' => 'Korea Północna',
+ 'KR' => 'Korea Południowa',
+ 'KW' => 'Kuwejt',
+ 'KG' => 'Kirgistan',
+ 'LA' => 'Laos',
+ 'LV' => 'Łotwa',
+ 'LB' => 'Liban',
+ 'LS' => 'Lesotho',
+ 'LR' => 'Liberia',
+ 'LY' => 'Libia',
+ 'LI' => 'Liechtenstein',
+ 'LT' => 'Litwa',
+ 'LU' => 'Luksemburg',
+ 'MO' => 'Makau',
+ 'MG' => 'Madagaskar',
+ 'MW' => 'Malawi',
+ 'MY' => 'Malezja',
+ 'MV' => 'Malediwy',
+ 'ML' => 'Mali',
+ 'MT' => 'Malta',
+ 'MH' => 'Wyspy Marshalla',
+ 'MQ' => 'Martynika',
+ 'MR' => 'Mauretania',
+ 'MU' => 'Mauritius',
+ 'YT' => 'Majotta',
+ 'MX' => 'Meksyk',
+ 'FM' => 'Mikronezja',
+ 'MD' => 'Mołdawia',
+ 'MC' => 'Monako',
+ 'MN' => 'Mongolia',
+ 'ME' => 'Czarnogóra',
+ 'MS' => 'Montserrat',
+ 'MA' => 'Maroko',
+ 'MZ' => 'Mozambik',
+ 'MM' => 'Mjanma',
+ 'NA' => 'Namibia',
+ 'NR' => 'Nauru',
+ 'NP' => 'Nepal',
+ 'NL' => 'Holandia',
+ 'NC' => 'Nowa Kaledonia',
+ 'NZ' => 'Nowa Zelandia',
+ 'NI' => 'Nikaragua',
+ 'NE' => 'Niger',
+ 'NG' => 'Nigeria',
+ 'NU' => 'Niue',
+ 'NF' => 'Norfolk',
+ 'MK' => 'Macedonia Północna',
+ 'MP' => 'Mariany Północne',
+ 'NO' => 'Norwegia',
+ 'OM' => 'Oman',
+ 'PK' => 'Pakistan',
+ 'PW' => 'Palau',
+ 'PS' => 'Palestyna',
+ 'PA' => 'Panama',
+ 'PG' => 'Papua-Nowa Gwinea',
+ 'PY' => 'Paragwaj',
+ 'PE' => 'Peru',
+ 'PH' => 'Filipiny',
+ 'PN' => 'Pitcairn',
+ 'PL' => 'Polska',
+ 'PT' => 'Portugalia',
+ 'PR' => 'Portoryko',
+ 'QA' => 'Katar',
+ 'RE' => 'Reunion',
+ 'RO' => 'Rumunia',
+ 'RU' => 'Rosja',
+ 'RW' => 'Rwanda',
+ 'BL' => 'Saint-Barthélemy',
+ 'SH' => 'Święta Helena, Wyspa Wniebowstąpienia i Tristan da Cunha',
+ 'KN' => 'Saint Kitts i Nevis',
+ 'LC' => 'Saint Lucia',
+ 'MF' => 'Saint-Martin',
+ 'PM' => 'Saint-Pierre i Miquelon',
+ 'VC' => 'Saint Vincent i Grenadyny',
+ 'WS' => 'Samoa',
+ 'SM' => 'San Marino',
+ 'ST' => 'Wyspy Świętego Tomasza i Książęca',
+ 'SA' => 'Arabia Saudyjska',
+ 'SN' => 'Senegal',
+ 'RS' => 'Serbia',
+ 'SC' => 'Seszele',
+ 'SL' => 'Sierra Leone',
+ 'SG' => 'Singapur',
+ 'SX' => 'Sint Maarten',
+ 'SK' => 'Słowacja',
+ 'SI' => 'Słowenia',
+ 'SB' => 'Wyspy Salomona',
+ 'SO' => 'Somalia',
+ 'ZA' => 'Południowa Afryka',
+ 'GS' => 'Georgia Południowa i Sandwich Południowy',
+ 'SS' => 'Sudan Południowy',
+ 'ES' => 'Hiszpania',
+ 'LK' => 'Sri Lanka',
+ 'SD' => 'Sudan',
+ 'SR' => 'Surinam',
+ 'SJ' => 'Svalbard i Jan Mayen',
+ 'SE' => 'Szwecja',
+ 'CH' => 'Szwajcaria',
+ 'SY' => 'Syria',
+ 'TW' => 'Tajwan',
+ 'TJ' => 'Tadżykistan',
+ 'TZ' => 'Tanzania',
+ 'TH' => 'Tajlandia',
+ 'TL' => 'Timor Wschodni',
+ 'TG' => 'Togo',
+ 'TK' => 'Tokelau',
+ 'TO' => 'Tonga',
+ 'TT' => 'Trynidad i Tobago',
+ 'TN' => 'Tunezja',
+ 'TR' => 'Turcja',
+ 'TM' => 'Turkmenistan',
+ 'TC' => 'Turks i Caicos',
+ 'TV' => 'Tuvalu',
+ 'UG' => 'Uganda',
+ 'UA' => 'Ukraina',
+ 'AE' => 'Zjednoczone Emiraty Arabskie',
+ 'GB' => 'Wielka Brytania',
+ 'UM' => 'Dalekie Wyspy Mniejsze Stanów Zjednoczonych',
+ 'US' => 'Stany Zjednoczone',
+ 'UY' => 'Urugwaj',
+ 'UZ' => 'Uzbekistan',
+ 'VU' => 'Vanuatu',
+ 'VE' => 'Wenezuela',
+ 'VN' => 'Wietnam',
+ 'VG' => 'Brytyjskie Wyspy Dziewicze',
+ 'VI' => 'Wyspy Dziewicze Stanów Zjednoczonych',
+ 'WF' => 'Wallis i Futuna',
+ 'EH' => 'Sahara Zachodnia',
+ 'YE' => 'Jemen',
+ 'ZM' => 'Zambia',
+ 'ZW' => 'Zimbabwe',
+];
diff --git a/resources/lang/pl/default.php b/resources/lang/pl/default.php
index 072a1a6..28f8061 100644
--- a/resources/lang/pl/default.php
+++ b/resources/lang/pl/default.php
@@ -24,6 +24,13 @@ return [
'forward_query_params' => 'Przekaż parametry URL',
'forward_query_params_helper' => 'Dołącz parametry wejściowe (np. tagi UTM) do docelowego adresu URL.',
'expires_at' => 'Wygasa dnia',
+ 'activated_at' => 'Aktywny od',
+ 'max_visits' => 'Maksymalny limit wejść',
+ 'max_visits_helper' => 'Opcjonalny limit liczby wejść, po osiągnięciu którego link zostanie automatycznie wyłączony.',
+ 'expiration_redirect_url' => 'URL przekierowania po wygaśnięciu',
+ 'expiration_redirect_url_helper' => 'Adres URL, na który nastąpi przekierowanie, gdy link wygaśnie lub będzie nieaktywny (pozostaw puste, aby wyświetlić błąd 410 Gone).',
+ 'form_section_validity' => 'Ważność i limity',
+ 'use_date_validity' => 'Ustaw limity datowe',
'notes' => 'Wewnętrzne notatki',
'ga_tracking_id' => 'Identyfikator śledzenia Google Analytics 4',
'ga_tracking_id_helper' => 'Opcjonalny identyfikator G-XXXXXXXXXX do śledzenia przekierowań po stronie serwera.',
@@ -289,6 +296,39 @@ return [
'settings_rate_limiting_decay_seconds' => 'Okno wygasania (sekundy)',
'settings_rate_limiting_decay_seconds_helper' => 'Przedział czasowy dla ograniczenia liczby żądań.',
+ // Queue Settings Additions
+ 'settings_queue_name' => 'Nazwa kolejki',
+ 'settings_queue_name_helper' => 'Docelowa nazwa kolejki, do której wysyłane są zadania śledzenia i synchronizacji liczników. Domyślnie: "default".',
+ 'settings_geoip_stats_cache_ttl' => 'TTL cache statystyk panelu',
+ 'settings_geoip_stats_cache_ttl_helper' => 'Liczba sekund buforowania wyników zapytań na stronie statystyk (zapobiega obciążeniu bazy danych). Domyślnie: 300 (5 minut).',
+
+ // Default Tracking Tab
+ 'settings_tab_tracking_defaults' => 'Domyślne śledzenie',
+ 'settings_section_tracking_defaults' => 'Domyślne ustawienia śledzenia wizyt',
+ 'settings_section_tracking_defaults_helper' => 'Ustawienia te określają domyślny status śledzenia dla nowo tworzonych krótkich adresów URL.',
+ 'settings_track_visits_default' => 'Śledź wizyty domyślnie',
+ 'settings_track_ip_default' => 'Domyślne śledzenie adresu IP',
+ 'settings_track_browser_default' => 'Domyślne śledzenie przeglądarki',
+ 'settings_track_browser_version_default' => 'Domyślne śledzenie wersji przeglądarki',
+ 'settings_track_os_default' => 'Domyślne śledzenie systemu operacyjnego',
+ 'settings_track_os_version_default' => 'Domyślne śledzenie wersji systemu operacyjnego',
+ 'settings_track_referer_default' => 'Domyślne śledzenie adresu referera',
+ 'settings_track_device_type_default' => 'Domyślne śledzenie typu urządzenia',
+
+ // QR Defaults Tab
+ 'settings_tab_qr' => 'Domyślne QR',
+ 'settings_section_qr_defaults' => 'Domyślne opcje wyglądu kodów QR',
+ 'settings_section_qr_defaults_helper' => 'Skonfiguruj standardowy styl wyglądu automatycznie ładowany dla nowo generowanych kodów QR.',
+ 'settings_qr_size' => 'Domyślny rozmiar (px)',
+ 'settings_qr_margin' => 'Domyślny margines',
+ 'settings_qr_dot_style' => 'Domyślny styl kropek',
+ 'settings_qr_foreground_color' => 'Domyślny kolor pierwszego planu',
+ 'settings_qr_background_color' => 'Domyślny kolor tła',
+ 'settings_qr_gradient_enabled' => 'Domyślnie włącz gradient',
+ 'settings_qr_gradient_from' => 'Kolor gradientu od',
+ 'settings_qr_gradient_to' => 'Kolor gradientu do',
+ 'settings_qr_gradient_type' => 'Domyślny typ gradientu',
+
// Views
'password_title' => 'Wymagane hasło',
'password_description' => 'Ten link jest chroniony hasłem. Wprowadź poprawne hasło, aby kontynuować.',
diff --git a/src/Filament/Resources/ShortUrlResource/Pages/ShortUrlSettingsPage.php b/src/Filament/Resources/ShortUrlResource/Pages/ShortUrlSettingsPage.php
index 6e92ce7..9075fec 100644
--- a/src/Filament/Resources/ShortUrlResource/Pages/ShortUrlSettingsPage.php
+++ b/src/Filament/Resources/ShortUrlResource/Pages/ShortUrlSettingsPage.php
@@ -5,6 +5,7 @@ namespace Bjanczak\FilamentShortUrl\Filament\Resources\ShortUrlResource\Pages;
use Bjanczak\FilamentShortUrl\Filament\Resources\ShortUrlResource;
use Bjanczak\FilamentShortUrl\Services\ShortUrlSettingsManager;
use Filament\Actions\Action;
+use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
@@ -44,7 +45,9 @@ class ShortUrlSettingsPage extends Page implements HasForms
'geo_ip_cache_ttl' => $mgr->get('geo_ip_cache_ttl', 86400),
'geo_ip_timeout' => $mgr->get('geo_ip_timeout', 3),
'maxmind_database_path' => $mgr->get('maxmind_database_path', storage_path('geoip/GeoLite2-Country.mmdb')),
+ 'geo_ip_stats_cache_ttl' => $mgr->get('geo_ip_stats_cache_ttl', 300),
'queue_connection' => $mgr->get('queue_connection', 'sync'),
+ 'queue_name' => $mgr->get('queue_name', 'default'),
'ga4_api_secret' => $mgr->get('ga4_api_secret'),
'ga4_firebase_app_id' => $mgr->get('ga4_firebase_app_id'),
'counter_buffering_enabled' => $mgr->get('counter_buffering_enabled', false),
@@ -54,6 +57,23 @@ class ShortUrlSettingsPage extends Page implements HasForms
'rate_limiting_enabled' => $mgr->get('rate_limiting_enabled', false),
'rate_limiting_max_attempts' => $mgr->get('rate_limiting_max_attempts', 60),
'rate_limiting_decay_seconds' => $mgr->get('rate_limiting_decay_seconds', 60),
+ 'tracking_enabled' => $mgr->get('tracking_enabled', true),
+ 'tracking_fields_ip_address' => $mgr->get('tracking_fields_ip_address', true),
+ 'tracking_fields_browser' => $mgr->get('tracking_fields_browser', true),
+ 'tracking_fields_browser_version' => $mgr->get('tracking_fields_browser_version', true),
+ 'tracking_fields_operating_system' => $mgr->get('tracking_fields_operating_system', true),
+ 'tracking_fields_operating_system_version' => $mgr->get('tracking_fields_operating_system_version', true),
+ 'tracking_fields_referer_url' => $mgr->get('tracking_fields_referer_url', true),
+ 'tracking_fields_device_type' => $mgr->get('tracking_fields_device_type', true),
+ 'qr_size' => $mgr->get('qr_size', 300),
+ 'qr_margin' => $mgr->get('qr_margin', 1),
+ 'qr_dot_style' => $mgr->get('qr_dot_style', 'square'),
+ 'qr_foreground_color' => $mgr->get('qr_foreground_color', '#000000'),
+ 'qr_background_color' => $mgr->get('qr_background_color', '#ffffff'),
+ 'qr_gradient_enabled' => $mgr->get('qr_gradient_enabled', false),
+ 'qr_gradient_from' => $mgr->get('qr_gradient_from', '#4f46e5'),
+ 'qr_gradient_to' => $mgr->get('qr_gradient_to', '#06b6d4'),
+ 'qr_gradient_type' => $mgr->get('qr_gradient_type', 'linear'),
]);
}
@@ -91,6 +111,7 @@ class ShortUrlSettingsPage extends Page implements HasForms
->label(__('filament-short-url::default.settings_key_length'))
->helperText(__('filament-short-url::default.settings_key_length_helper'))
->numeric()
+ ->integer()
->minValue(4)
->maxValue(20)
->required(),
@@ -99,6 +120,7 @@ class ShortUrlSettingsPage extends Page implements HasForms
->label(__('filament-short-url::default.settings_cache_ttl'))
->helperText(__('filament-short-url::default.settings_cache_ttl_helper'))
->numeric()
+ ->integer()
->minValue(0)
->suffix('s')
->required(),
@@ -111,6 +133,7 @@ class ShortUrlSettingsPage extends Page implements HasForms
]),
Section::make(__('filament-short-url::default.settings_section_queue'))
+ ->columns(2)
->schema([
Select::make('queue_connection')
->label(__('filament-short-url::default.settings_queue_connection'))
@@ -125,6 +148,11 @@ class ShortUrlSettingsPage extends Page implements HasForms
];
})
->required(),
+
+ TextInput::make('queue_name')
+ ->label(__('filament-short-url::default.settings_queue_name'))
+ ->helperText(__('filament-short-url::default.settings_queue_name_helper'))
+ ->required(),
]),
Section::make(__('filament-short-url::default.settings_section_buffering'))
@@ -168,6 +196,18 @@ class ShortUrlSettingsPage extends Page implements HasForms
->label(__('filament-short-url::default.settings_geoip_cache_ttl'))
->helperText(__('filament-short-url::default.settings_geoip_cache_ttl_helper'))
->numeric()
+ ->integer()
+ ->minValue(0)
+ ->suffix('s')
+ ->required()
+ ->visible(fn (Get $get): bool => (bool) $get('geo_ip_enabled')),
+
+ // ── Stats Cache TTL (only when geo-ip is on) ──
+ TextInput::make('geo_ip_stats_cache_ttl')
+ ->label(__('filament-short-url::default.settings_geoip_stats_cache_ttl'))
+ ->helperText(__('filament-short-url::default.settings_geoip_stats_cache_ttl_helper'))
+ ->numeric()
+ ->integer()
->minValue(0)
->suffix('s')
->required()
@@ -178,6 +218,7 @@ class ShortUrlSettingsPage extends Page implements HasForms
->label(__('filament-short-url::default.settings_geoip_timeout'))
->helperText(__('filament-short-url::default.settings_geoip_timeout_helper'))
->numeric()
+ ->integer()
->minValue(1)
->maxValue(30)
->suffix('s')
@@ -354,6 +395,7 @@ class ShortUrlSettingsPage extends Page implements HasForms
->label(__('filament-short-url::default.settings_rate_limiting_max_attempts'))
->helperText(__('filament-short-url::default.settings_rate_limiting_max_attempts_helper'))
->numeric()
+ ->integer()
->minValue(1)
->required()
->visible(fn (Get $get): bool => (bool) $get('rate_limiting_enabled')),
@@ -362,12 +404,139 @@ class ShortUrlSettingsPage extends Page implements HasForms
->label(__('filament-short-url::default.settings_rate_limiting_decay_seconds'))
->helperText(__('filament-short-url::default.settings_rate_limiting_decay_seconds_helper'))
->numeric()
+ ->integer()
->minValue(1)
->suffix('s')
->required()
->visible(fn (Get $get): bool => (bool) $get('rate_limiting_enabled')),
]),
]),
+
+ // ── Tracking Defaults ─────────────────────────────────
+ Tab::make(__('filament-short-url::default.settings_tab_tracking_defaults'))
+ ->icon('heroicon-o-chart-bar')
+ ->schema([
+ Section::make(__('filament-short-url::default.settings_section_tracking_defaults'))
+ ->description(__('filament-short-url::default.settings_section_tracking_defaults_helper'))
+ ->schema([
+ Toggle::make('tracking_enabled')
+ ->label(__('filament-short-url::default.settings_track_visits_default'))
+ ->live()
+ ->inline(false)
+ ->columnSpanFull(),
+
+ Toggle::make('tracking_fields_ip_address')
+ ->label(__('filament-short-url::default.settings_track_ip_default'))
+ ->inline(false)
+ ->disabled(fn (Get $get): bool => ! $get('tracking_enabled')),
+
+ Toggle::make('tracking_fields_browser')
+ ->label(__('filament-short-url::default.settings_track_browser_default'))
+ ->inline(false)
+ ->disabled(fn (Get $get): bool => ! $get('tracking_enabled')),
+
+ Toggle::make('tracking_fields_browser_version')
+ ->label(__('filament-short-url::default.settings_track_browser_version_default'))
+ ->inline(false)
+ ->disabled(fn (Get $get): bool => ! $get('tracking_enabled')),
+
+ Toggle::make('tracking_fields_operating_system')
+ ->label(__('filament-short-url::default.settings_track_os_default'))
+ ->inline(false)
+ ->disabled(fn (Get $get): bool => ! $get('tracking_enabled')),
+
+ Toggle::make('tracking_fields_operating_system_version')
+ ->label(__('filament-short-url::default.settings_track_os_version_default'))
+ ->inline(false)
+ ->disabled(fn (Get $get): bool => ! $get('tracking_enabled')),
+
+ Toggle::make('tracking_fields_referer_url')
+ ->label(__('filament-short-url::default.settings_track_referer_default'))
+ ->inline(false)
+ ->disabled(fn (Get $get): bool => ! $get('tracking_enabled')),
+
+ Toggle::make('tracking_fields_device_type')
+ ->label(__('filament-short-url::default.settings_track_device_type_default'))
+ ->inline(false)
+ ->disabled(fn (Get $get): bool => ! $get('tracking_enabled')),
+ ])
+ ->columns(4),
+ ]),
+
+ // ── QR Defaults ──────────────────────────────────────
+ Tab::make(__('filament-short-url::default.settings_tab_qr'))
+ ->icon('heroicon-o-qr-code')
+ ->schema([
+ Section::make(__('filament-short-url::default.settings_section_qr_defaults'))
+ ->description(__('filament-short-url::default.settings_section_qr_defaults_helper'))
+ ->columns(2)
+ ->schema([
+ TextInput::make('qr_size')
+ ->label(__('filament-short-url::default.settings_qr_size'))
+ ->numeric()
+ ->integer()
+ ->minValue(100)
+ ->maxValue(2000)
+ ->required(),
+
+ Select::make('qr_margin')
+ ->label(__('filament-short-url::default.settings_qr_margin'))
+ ->options(array_combine(range(0, 10), range(0, 10)))
+ ->required(),
+
+ Select::make('qr_dot_style')
+ ->label(__('filament-short-url::default.settings_qr_dot_style'))
+ ->options([
+ 'square' => __('filament-short-url::default.qr_option_square'),
+ 'dots' => __('filament-short-url::default.qr_option_dots'),
+ 'rounded' => __('filament-short-url::default.qr_option_rounded'),
+ 'classy' => __('filament-short-url::default.qr_option_classy'),
+ 'classy-rounded' => __('filament-short-url::default.qr_option_classy_rounded'),
+ 'extra-rounded' => __('filament-short-url::default.qr_option_extra_rounded'),
+ ])
+ ->required(),
+
+ ColorPicker::make('qr_foreground_color')
+ ->label(__('filament-short-url::default.settings_qr_foreground_color'))
+ ->regex('/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})\b$/')
+ ->placeholder('#000000')
+ ->required(),
+
+ ColorPicker::make('qr_background_color')
+ ->label(__('filament-short-url::default.settings_qr_background_color'))
+ ->regex('/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})\b$/')
+ ->placeholder('#ffffff')
+ ->required(),
+
+ Toggle::make('qr_gradient_enabled')
+ ->label(__('filament-short-url::default.settings_qr_gradient_enabled'))
+ ->live()
+ ->inline(false),
+
+ ColorPicker::make('qr_gradient_from')
+ ->label(__('filament-short-url::default.settings_qr_gradient_from'))
+ ->regex('/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})\b$/')
+ ->placeholder('#4f46e5')
+ ->required(fn (Get $get): bool => (bool) $get('qr_gradient_enabled'))
+ ->visible(fn (Get $get): bool => (bool) $get('qr_gradient_enabled')),
+
+ ColorPicker::make('qr_gradient_to')
+ ->label(__('filament-short-url::default.settings_qr_gradient_to'))
+ ->regex('/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})\b$/')
+ ->placeholder('#06b6d4')
+ ->required(fn (Get $get): bool => (bool) $get('qr_gradient_enabled'))
+ ->visible(fn (Get $get): bool => (bool) $get('qr_gradient_enabled')),
+
+ Select::make('qr_gradient_type')
+ ->label(__('filament-short-url::default.settings_qr_gradient_type'))
+ ->options([
+ 'linear' => __('filament-short-url::default.qr_gradient_linear'),
+ 'radial' => __('filament-short-url::default.qr_gradient_radial'),
+ ])
+ ->required(fn (Get $get): bool => (bool) $get('qr_gradient_enabled'))
+ ->visible(fn (Get $get): bool => (bool) $get('qr_gradient_enabled')),
+ ]),
+ ]),
]),
])
->statePath('data');
diff --git a/src/Filament/Resources/ShortUrlResource/Schemas/ShortUrlForm.php b/src/Filament/Resources/ShortUrlResource/Schemas/ShortUrlForm.php
index 6f4dec7..ca99af2 100644
--- a/src/Filament/Resources/ShortUrlResource/Schemas/ShortUrlForm.php
+++ b/src/Filament/Resources/ShortUrlResource/Schemas/ShortUrlForm.php
@@ -103,34 +103,89 @@ class ShortUrlForm
302 => __('filament-short-url::default.redirect_code_302'),
301 => __('filament-short-url::default.redirect_code_301'),
])
- ->default(302)
+ ->default(fn () => config('filament-short-url.redirect_status_code', 302))
->required(),
])->columns(2),
- Section::make(__('filament-short-url::default.form_section_options'))->schema([
+ Section::make(__('filament-short-url::default.form_section_options'))->schema([
Toggle::make('is_enabled')
->label(__('filament-short-url::default.status'))
->default(true)
->inline(false),
- Toggle::make('single_use')
- ->label(__('filament-short-url::default.single_use'))
- ->helperText(__('filament-short-url::default.single_use_helper'))
- ->default(false)
- ->inline(false),
-
Toggle::make('forward_query_params')
->label(__('filament-short-url::default.forward_query_params'))
->helperText(__('filament-short-url::default.forward_query_params_helper'))
->default(false)
->inline(false),
-
- DateTimePicker::make('expires_at')
- ->label(__('filament-short-url::default.expires_at'))
- ->nullable()
- ->native(false),
])->columns(2),
+ Section::make(__('filament-short-url::default.form_section_validity'))
+ ->schema([
+ Toggle::make('use_date_validity')
+ ->label(__('filament-short-url::default.use_date_validity'))
+ ->dehydrated(false)
+ ->live()
+ ->afterStateHydrated(function (Toggle $component, $state, Get $get, Set $set) {
+ $set('use_date_validity', $get('activated_at') !== null || $get('expires_at') !== null);
+ })
+ ->afterStateUpdated(function ($state, Set $set) {
+ if ($state) {
+ $set('activated_at', now()->startOfMinute());
+ } else {
+ $set('activated_at', null);
+ $set('expires_at', null);
+ $set('expiration_redirect_url', null);
+ }
+ })
+ ->columnSpanFull(),
+
+ DateTimePicker::make('activated_at')
+ ->label(__('filament-short-url::default.activated_at'))
+ ->nullable()
+ ->native(false)
+ ->withoutSeconds()
+ ->live(onBlur: true)
+ ->required(fn (Get $get): bool => (bool) $get('use_date_validity'))
+ ->visible(fn (Get $get): bool => (bool) $get('use_date_validity'))
+ ->minDate(now()->startOfDay())
+ ->maxDate(fn (Get $get) => $get('expires_at')),
+
+ DateTimePicker::make('expires_at')
+ ->label(__('filament-short-url::default.expires_at'))
+ ->nullable()
+ ->native(false)
+ ->withoutSeconds()
+ ->live(onBlur: true)
+ ->visible(fn (Get $get): bool => (bool) $get('use_date_validity'))
+ ->minDate(fn (Get $get) => $get('activated_at') ?: now()->startOfDay()),
+
+ TextInput::make('expiration_redirect_url')
+ ->label(__('filament-short-url::default.expiration_redirect_url'))
+ ->helperText(__('filament-short-url::default.expiration_redirect_url_helper'))
+ ->url()
+ ->maxLength(2048)
+ ->nullable()
+ ->visible(fn (Get $get): bool => (bool) $get('use_date_validity'))
+ ->columnSpanFull(),
+
+ Toggle::make('single_use')
+ ->label(__('filament-short-url::default.single_use'))
+ ->helperText(__('filament-short-url::default.single_use_helper'))
+ ->default(false)
+ ->inline(false)
+ ->live(),
+
+ TextInput::make('max_visits')
+ ->label(__('filament-short-url::default.max_visits'))
+ ->helperText(__('filament-short-url::default.max_visits_helper'))
+ ->numeric()
+ ->integer()
+ ->minValue(1)
+ ->nullable()
+ ->hidden(fn (Get $get): bool => (bool) $get('single_use')),
+ ])->columns(2),
+
Section::make(__('filament-short-url::default.form_section_notes'))->schema([
Textarea::make('notes')
->label(__('filament-short-url::default.notes'))
@@ -183,7 +238,7 @@ class ShortUrlForm
->schema([
Toggle::make('track_visits')
->label(__('filament-short-url::default.track_visits'))
- ->default(true)
+ ->default(fn () => config('filament-short-url.tracking.enabled', true))
->live()
->inline(false)
->columnSpanFull(),
@@ -193,43 +248,43 @@ class ShortUrlForm
->schema([
Toggle::make('track_ip_address')
->label(__('filament-short-url::default.track_ip'))
- ->default(true)
+ ->default(fn () => config('filament-short-url.tracking.fields.ip_address', true))
->inline(false)
->disabled(fn (Get $get): bool => ! $get('track_visits')),
Toggle::make('track_browser')
->label(__('filament-short-url::default.track_browser'))
- ->default(true)
+ ->default(fn () => config('filament-short-url.tracking.fields.browser', true))
->inline(false)
->disabled(fn (Get $get): bool => ! $get('track_visits')),
Toggle::make('track_browser_version')
->label(__('filament-short-url::default.track_browser_version'))
- ->default(true)
+ ->default(fn () => config('filament-short-url.tracking.fields.browser_version', true))
->inline(false)
->disabled(fn (Get $get): bool => ! $get('track_visits')),
Toggle::make('track_operating_system')
->label(__('filament-short-url::default.track_os'))
- ->default(true)
+ ->default(fn () => config('filament-short-url.tracking.fields.operating_system', true))
->inline(false)
->disabled(fn (Get $get): bool => ! $get('track_visits')),
Toggle::make('track_operating_system_version')
->label(__('filament-short-url::default.track_os_version'))
- ->default(true)
+ ->default(fn () => config('filament-short-url.tracking.fields.operating_system_version', true))
->inline(false)
->disabled(fn (Get $get): bool => ! $get('track_visits')),
Toggle::make('track_device_type')
->label(__('filament-short-url::default.track_device_type'))
- ->default(true)
+ ->default(fn () => config('filament-short-url.tracking.fields.device_type', true))
->inline(false)
->disabled(fn (Get $get): bool => ! $get('track_visits')),
Toggle::make('track_referer_url')
->label(__('filament-short-url::default.track_referer'))
- ->default(true)
+ ->default(fn () => config('filament-short-url.tracking.fields.referer_url', true))
->inline(false)
->disabled(fn (Get $get): bool => ! $get('track_visits')),
])
@@ -378,39 +433,17 @@ class ShortUrlForm
->schema([
Select::make('country_code')
->label(__('filament-short-url::default.country_code'))
- ->options([
- 'PL' => 'Poland',
- 'US' => 'United States',
- 'GB' => 'United Kingdom',
- 'DE' => 'Germany',
- 'FR' => 'France',
- 'ES' => 'Spain',
- 'IT' => 'Italy',
- 'CA' => 'Canada',
- 'AU' => 'Australia',
- 'NL' => 'Netherlands',
- 'UA' => 'Ukraine',
- 'IE' => 'Ireland',
- 'BE' => 'Belgium',
- 'AT' => 'Austria',
- 'CH' => 'Switzerland',
- 'SE' => 'Sweden',
- 'NO' => 'Norway',
- 'DK' => 'Denmark',
- 'FI' => 'Finland',
- 'CZ' => 'Czech Republic',
- 'SK' => 'Slovakia',
- 'HU' => 'Hungary',
- 'RO' => 'Romania',
- 'GR' => 'Greece',
- 'PT' => 'Portugal',
- 'BR' => 'Brazil',
- 'MX' => 'Mexico',
- 'CN' => 'China',
- 'JP' => 'Japan',
- 'IN' => 'India',
- ])
+ ->options(function (): array {
+ $countries = __('filament-short-url::countries');
+ if (is_array($countries)) {
+ asort($countries, SORT_LOCALE_STRING);
+ return $countries;
+ }
+ return [];
+ })
->searchable()
+ ->optionsLimit(300)
+ ->disableOptionsWhenSelectedInSiblingRepeaterItems()
->required(),
TextInput::make('url')
->label(__('filament-short-url::default.destination_url'))
diff --git a/src/FilamentShortUrlServiceProvider.php b/src/FilamentShortUrlServiceProvider.php
index 0278896..6908043 100644
--- a/src/FilamentShortUrlServiceProvider.php
+++ b/src/FilamentShortUrlServiceProvider.php
@@ -31,6 +31,7 @@ class FilamentShortUrlServiceProvider extends PackageServiceProvider
'2026_06_01_000003_add_utm_city_referer_to_short_url_visits_table',
'2026_06_01_000004_add_targeting_and_security_to_short_urls_table',
'2026_06_01_000005_create_short_url_daily_stats_table',
+ '2026_06_02_000006_add_max_visits_and_expiration_redirect_to_short_urls_table',
])
->hasCommands([
SyncBufferedCountersCommand::class,
diff --git a/src/Http/Controllers/ShortUrlRedirectController.php b/src/Http/Controllers/ShortUrlRedirectController.php
index a2b39c0..384b55a 100644
--- a/src/Http/Controllers/ShortUrlRedirectController.php
+++ b/src/Http/Controllers/ShortUrlRedirectController.php
@@ -27,8 +27,12 @@ class ShortUrlRedirectController extends Controller
abort(404);
}
- // 410 Gone if disabled or expired
+ // Redirect to custom expiration URL if defined, otherwise 410 Gone if disabled or expired
if (! $shortUrl->isActive()) {
+ if ($shortUrl->expiration_redirect_url) {
+ return redirect()->away($shortUrl->expiration_redirect_url, 302);
+ }
+
abort(410);
}
diff --git a/src/Models/ShortUrl.php b/src/Models/ShortUrl.php
index 90d8320..b6029e9 100644
--- a/src/Models/ShortUrl.php
+++ b/src/Models/ShortUrl.php
@@ -78,6 +78,8 @@ class ShortUrl extends Model
'targeting_rules',
'total_visits',
'unique_visits',
+ 'max_visits',
+ 'expiration_redirect_url',
];
/** @var array */
@@ -96,6 +98,7 @@ class ShortUrl extends Model
'qr_options' => 'array',
'show_warning_page' => 'boolean',
'targeting_rules' => 'array',
+ 'max_visits' => 'integer',
'activated_at' => 'datetime',
'deactivated_at' => 'datetime',
'expires_at' => 'datetime',
@@ -124,9 +127,21 @@ class ShortUrl extends Model
{
return $query
->enabled()
+ ->where(fn (Builder $q) => $q
+ ->whereNull('activated_at')
+ ->orWhere('activated_at', '<=', now())
+ )
+ ->where(fn (Builder $q) => $q
+ ->whereNull('deactivated_at')
+ ->orWhere('deactivated_at', '>', now())
+ )
->where(fn (Builder $q) => $q
->whereNull('expires_at')
->orWhere('expires_at', '>', now())
+ )
+ ->where(fn (Builder $q) => $q
+ ->whereNull('max_visits')
+ ->orWhereRaw('total_visits < max_visits')
);
}
@@ -163,6 +178,16 @@ class ShortUrl extends Model
*/
protected static function booted(): void
{
+ static::saving(function (self $m) {
+ if ($m->single_use) {
+ $m->max_visits = null;
+ }
+
+ if ($m->activated_at === null && $m->expires_at === null) {
+ $m->expiration_redirect_url = null;
+ }
+ });
+
static::saved(fn (self $m) => cache()->forget("filament-short-url:{$m->url_key}"));
static::deleted(fn (self $m) => cache()->forget("filament-short-url:{$m->url_key}"));
@@ -210,12 +235,25 @@ class ShortUrl extends Model
return false;
}
+ // Visit limit reached
+ if (! $this->single_use && $this->max_visits !== null && $this->total_visits >= $this->max_visits) {
+ return false;
+ }
+
return true;
}
public function isExpired(): bool
{
- return $this->expires_at !== null && $this->expires_at->isPast();
+ if ($this->expires_at !== null && $this->expires_at->isPast()) {
+ return true;
+ }
+
+ if (! $this->single_use && $this->max_visits !== null && $this->total_visits >= $this->max_visits) {
+ return true;
+ }
+
+ return false;
}
public function trackingEnabled(): bool
diff --git a/src/Services/ShortUrlBuilder.php b/src/Services/ShortUrlBuilder.php
index 8b71b6b..7776810 100644
--- a/src/Services/ShortUrlBuilder.php
+++ b/src/Services/ShortUrlBuilder.php
@@ -51,6 +51,34 @@ class ShortUrlBuilder
return $this;
}
+ public function activatedAt(\DateTimeInterface|Carbon|null $date): static
+ {
+ $this->data['activated_at'] = $date ? Carbon::instance($date) : null;
+
+ return $this;
+ }
+
+ public function deactivatedAt(\DateTimeInterface|Carbon|null $date): static
+ {
+ $this->data['deactivated_at'] = $date ? Carbon::instance($date) : null;
+
+ return $this;
+ }
+
+ public function maxVisits(?int $maxVisits): static
+ {
+ $this->data['max_visits'] = $maxVisits;
+
+ return $this;
+ }
+
+ public function expirationRedirectUrl(?string $url): static
+ {
+ $this->data['expiration_redirect_url'] = $url;
+
+ return $this;
+ }
+
public function trackVisits(bool $track = true): static
{
$this->data['track_visits'] = $track;
diff --git a/src/Services/ShortUrlSettingsManager.php b/src/Services/ShortUrlSettingsManager.php
index 4ed97fb..6a4e078 100644
--- a/src/Services/ShortUrlSettingsManager.php
+++ b/src/Services/ShortUrlSettingsManager.php
@@ -2,6 +2,7 @@
namespace Bjanczak\FilamentShortUrl\Services;
+use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
class ShortUrlSettingsManager
@@ -45,9 +46,11 @@ class ShortUrlSettingsManager
'geo_ip_cache_ttl' => config('filament-short-url.geo_ip.cache_ttl', 86400),
'geo_ip_timeout' => config('filament-short-url.geo_ip.timeout', 3),
'maxmind_database_path' => config('filament-short-url.geo_ip.maxmind.database_path', storage_path('geoip/GeoLite2-Country.mmdb')),
+ 'geo_ip_stats_cache_ttl' => config('filament-short-url.geo_ip.stats_cache_ttl', 300),
'ga4_api_secret' => config('filament-short-url.ga4.api_secret'),
'ga4_firebase_app_id' => config('filament-short-url.ga4.firebase_app_id'),
'queue_connection' => config('filament-short-url.queue_connection', 'sync'),
+ 'queue_name' => config('filament-short-url.queue_name', 'default'),
'cache_ttl' => config('filament-short-url.cache_ttl', 3600),
'counter_buffering_enabled' => config('filament-short-url.counter_buffering.enabled', false),
'trust_cdn_headers' => config('filament-short-url.trust_cdn_headers', false),
@@ -56,6 +59,23 @@ class ShortUrlSettingsManager
'rate_limiting_enabled' => config('filament-short-url.rate_limiting.enabled', false),
'rate_limiting_max_attempts' => config('filament-short-url.rate_limiting.max_attempts', 60),
'rate_limiting_decay_seconds' => config('filament-short-url.rate_limiting.decay_seconds', 60),
+ 'tracking_enabled' => config('filament-short-url.tracking.enabled', true),
+ 'tracking_fields_ip_address' => config('filament-short-url.tracking.fields.ip_address', true),
+ 'tracking_fields_browser' => config('filament-short-url.tracking.fields.browser', true),
+ 'tracking_fields_browser_version' => config('filament-short-url.tracking.fields.browser_version', true),
+ 'tracking_fields_operating_system' => config('filament-short-url.tracking.fields.operating_system', true),
+ 'tracking_fields_operating_system_version' => config('filament-short-url.tracking.fields.operating_system_version', true),
+ 'tracking_fields_referer_url' => config('filament-short-url.tracking.fields.referer_url', true),
+ 'tracking_fields_device_type' => config('filament-short-url.tracking.fields.device_type', true),
+ 'qr_size' => config('filament-short-url.qr_defaults.size', 300),
+ 'qr_margin' => config('filament-short-url.qr_defaults.margin', 1),
+ 'qr_dot_style' => config('filament-short-url.qr_defaults.dot_style', 'square'),
+ 'qr_foreground_color' => config('filament-short-url.qr_defaults.foreground_color', '#000000'),
+ 'qr_background_color' => config('filament-short-url.qr_defaults.background_color', '#ffffff'),
+ 'qr_gradient_enabled' => config('filament-short-url.qr_defaults.gradient_enabled', false),
+ 'qr_gradient_from' => config('filament-short-url.qr_defaults.gradient_from', '#4f46e5'),
+ 'qr_gradient_to' => config('filament-short-url.qr_defaults.gradient_to', '#06b6d4'),
+ 'qr_gradient_type' => config('filament-short-url.qr_defaults.gradient_type', 'linear'),
], $stored);
return $this->cache;
@@ -80,6 +100,8 @@ class ShortUrlSettingsManager
File::makeDirectory($dir, 0755, true);
}
+ $oldPrefix = $this->get('route_prefix');
+
// Keep only supported settings keys to prevent bloat
$keys = [
'route_prefix',
@@ -90,9 +112,11 @@ class ShortUrlSettingsManager
'geo_ip_cache_ttl',
'geo_ip_timeout',
'maxmind_database_path',
+ 'geo_ip_stats_cache_ttl',
'ga4_api_secret',
'ga4_firebase_app_id',
'queue_connection',
+ 'queue_name',
'cache_ttl',
'counter_buffering_enabled',
'trust_cdn_headers',
@@ -101,6 +125,23 @@ class ShortUrlSettingsManager
'rate_limiting_enabled',
'rate_limiting_max_attempts',
'rate_limiting_decay_seconds',
+ 'tracking_enabled',
+ 'tracking_fields_ip_address',
+ 'tracking_fields_browser',
+ 'tracking_fields_browser_version',
+ 'tracking_fields_operating_system',
+ 'tracking_fields_operating_system_version',
+ 'tracking_fields_referer_url',
+ 'tracking_fields_device_type',
+ 'qr_size',
+ 'qr_margin',
+ 'qr_dot_style',
+ 'qr_foreground_color',
+ 'qr_background_color',
+ 'qr_gradient_enabled',
+ 'qr_gradient_from',
+ 'qr_gradient_to',
+ 'qr_gradient_type',
];
$filtered = array_intersect_key($data, array_flip($keys));
@@ -121,6 +162,9 @@ class ShortUrlSettingsManager
if (isset($filtered['geo_ip_timeout'])) {
$filtered['geo_ip_timeout'] = (int) $filtered['geo_ip_timeout'];
}
+ if (isset($filtered['geo_ip_stats_cache_ttl'])) {
+ $filtered['geo_ip_stats_cache_ttl'] = (int) $filtered['geo_ip_stats_cache_ttl'];
+ }
if (isset($filtered['cache_ttl'])) {
$filtered['cache_ttl'] = (int) $filtered['cache_ttl'];
}
@@ -146,11 +190,60 @@ class ShortUrlSettingsManager
$filtered['rate_limiting_decay_seconds'] = (int) $filtered['rate_limiting_decay_seconds'];
}
+ // Tracking defaults
+ if (isset($filtered['tracking_enabled'])) {
+ $filtered['tracking_enabled'] = (bool) $filtered['tracking_enabled'];
+ }
+ if (isset($filtered['tracking_fields_ip_address'])) {
+ $filtered['tracking_fields_ip_address'] = (bool) $filtered['tracking_fields_ip_address'];
+ }
+ if (isset($filtered['tracking_fields_browser'])) {
+ $filtered['tracking_fields_browser'] = (bool) $filtered['tracking_fields_browser'];
+ }
+ if (isset($filtered['tracking_fields_browser_version'])) {
+ $filtered['tracking_fields_browser_version'] = (bool) $filtered['tracking_fields_browser_version'];
+ }
+ if (isset($filtered['tracking_fields_operating_system'])) {
+ $filtered['tracking_fields_operating_system'] = (bool) $filtered['tracking_fields_operating_system'];
+ }
+ if (isset($filtered['tracking_fields_operating_system_version'])) {
+ $filtered['tracking_fields_operating_system_version'] = (bool) $filtered['tracking_fields_operating_system_version'];
+ }
+ if (isset($filtered['tracking_fields_referer_url'])) {
+ $filtered['tracking_fields_referer_url'] = (bool) $filtered['tracking_fields_referer_url'];
+ }
+ if (isset($filtered['tracking_fields_device_type'])) {
+ $filtered['tracking_fields_device_type'] = (bool) $filtered['tracking_fields_device_type'];
+ }
+
+ // QR defaults
+ if (isset($filtered['qr_size'])) {
+ $filtered['qr_size'] = (int) $filtered['qr_size'];
+ }
+ if (isset($filtered['qr_margin'])) {
+ $filtered['qr_margin'] = (int) $filtered['qr_margin'];
+ }
+ if (isset($filtered['qr_gradient_enabled'])) {
+ $filtered['qr_gradient_enabled'] = (bool) $filtered['qr_gradient_enabled'];
+ }
+
File::put($path, json_encode($filtered, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
$this->cache = null;
// Apply immediately to current request config
$this->applyConfigOverrides();
+
+ // Clear route cache if route prefix has changed
+ $newPrefix = $filtered['route_prefix'] ?? null;
+ if ($oldPrefix !== null && $newPrefix !== null && $oldPrefix !== $newPrefix) {
+ try {
+ if (app()->routesAreCached()) {
+ Artisan::call('route:clear');
+ }
+ } catch (\Throwable) {
+ // Ignore route clear errors during boot/test
+ }
+ }
}
/**
@@ -169,9 +262,11 @@ class ShortUrlSettingsManager
'filament-short-url.geo_ip.cache_ttl' => $settings['geo_ip_cache_ttl'],
'filament-short-url.geo_ip.timeout' => $settings['geo_ip_timeout'],
'filament-short-url.geo_ip.maxmind.database_path' => $settings['maxmind_database_path'],
+ 'filament-short-url.geo_ip.stats_cache_ttl' => $settings['geo_ip_stats_cache_ttl'],
'filament-short-url.ga4.api_secret' => $settings['ga4_api_secret'],
'filament-short-url.ga4.firebase_app_id' => $settings['ga4_firebase_app_id'],
'filament-short-url.queue_connection' => $settings['queue_connection'],
+ 'filament-short-url.queue_name' => $settings['queue_name'],
'filament-short-url.cache_ttl' => $settings['cache_ttl'],
'filament-short-url.counter_buffering.enabled' => $settings['counter_buffering_enabled'],
'filament-short-url.trust_cdn_headers' => $settings['trust_cdn_headers'],
@@ -180,6 +275,23 @@ class ShortUrlSettingsManager
'filament-short-url.rate_limiting.enabled' => $settings['rate_limiting_enabled'],
'filament-short-url.rate_limiting.max_attempts' => $settings['rate_limiting_max_attempts'],
'filament-short-url.rate_limiting.decay_seconds' => $settings['rate_limiting_decay_seconds'],
+ 'filament-short-url.tracking.enabled' => $settings['tracking_enabled'],
+ 'filament-short-url.tracking.fields.ip_address' => $settings['tracking_fields_ip_address'],
+ 'filament-short-url.tracking.fields.browser' => $settings['tracking_fields_browser'],
+ 'filament-short-url.tracking.fields.browser_version' => $settings['tracking_fields_browser_version'],
+ 'filament-short-url.tracking.fields.operating_system' => $settings['tracking_fields_operating_system'],
+ 'filament-short-url.tracking.fields.operating_system_version' => $settings['tracking_fields_operating_system_version'],
+ 'filament-short-url.tracking.fields.referer_url' => $settings['tracking_fields_referer_url'],
+ 'filament-short-url.tracking.fields.device_type' => $settings['tracking_fields_device_type'],
+ 'filament-short-url.qr_defaults.size' => $settings['qr_size'],
+ 'filament-short-url.qr_defaults.margin' => $settings['qr_margin'],
+ 'filament-short-url.qr_defaults.dot_style' => $settings['qr_dot_style'],
+ 'filament-short-url.qr_defaults.foreground_color' => $settings['qr_foreground_color'],
+ 'filament-short-url.qr_defaults.background_color' => $settings['qr_background_color'],
+ 'filament-short-url.qr_defaults.gradient_enabled' => $settings['qr_gradient_enabled'],
+ 'filament-short-url.qr_defaults.gradient_from' => $settings['qr_gradient_from'],
+ 'filament-short-url.qr_defaults.gradient_to' => $settings['qr_gradient_to'],
+ 'filament-short-url.qr_defaults.gradient_type' => $settings['qr_gradient_type'],
]);
}
}