Improved MacOS support and organized screens and utils

This commit is contained in:
nullpeer
2025-11-30 12:49:33 +03:00
parent ae6fd57040
commit d793498d0a
56 changed files with 255 additions and 63 deletions

View File

@@ -0,0 +1,678 @@
class DevicePreset {
final String deviceType;
final String userAgent;
final String deviceName;
final String osVersion;
final String screen;
final String timezone;
final String locale;
DevicePreset({
required this.deviceType,
required this.userAgent,
required this.deviceName,
required this.osVersion,
required this.screen,
required this.timezone,
required this.locale,
});
}
final List<DevicePreset> devicePresets = [
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 14; SM-S928B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36',
deviceName: 'Samsung Galaxy S24 Ultra',
osVersion: 'Android 14',
screen: '1440x3120 2.8x',
timezone: 'Europe/Berlin',
locale: 'de-DE',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36',
deviceName: 'Google Pixel 8 Pro',
osVersion: 'Android 14',
screen: '1344x2992 2.7x',
timezone: 'America/New_York',
locale: 'en-US',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 13; 23021RAA2Y) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36',
deviceName: 'Xiaomi 13 Pro',
osVersion: 'Android 13',
screen: '1440x3200 2.9x',
timezone: 'Asia/Shanghai',
locale: 'zh-CN',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 14; CPH2521) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36',
deviceName: 'OnePlus 12',
osVersion: 'Android 14',
screen: '1440x3168 2.8x',
timezone: 'Asia/Kolkata',
locale: 'en-IN',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 13; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36',
deviceName: 'Samsung Galaxy S21 Ultra',
osVersion: 'Android 13',
screen: '1440x3200 2.9x',
timezone: 'Europe/London',
locale: 'en-GB',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
deviceName: 'Google Pixel 6',
osVersion: 'Android 12',
screen: '1080x2400 2.6x',
timezone: 'America/Chicago',
locale: 'en-US',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 13; RMX3371) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36',
deviceName: 'Realme GT Master Edition',
osVersion: 'Android 13',
screen: '1080x2400 2.5x',
timezone: 'Asia/Dubai',
locale: 'ar-AE',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 11; M2101K6G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Mobile Safari/537.36',
deviceName: 'Poco F3',
osVersion: 'Android 11',
screen: '1080x2400 2.6x',
timezone: 'Europe/Madrid',
locale: 'es-ES',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 14; SO-51D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36',
deviceName: 'Sony Xperia 1 V',
osVersion: 'Android 14',
screen: '1644x3840 3.5x',
timezone: 'Asia/Tokyo',
locale: 'ja-JP',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 13; XT2201-2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36',
deviceName: 'Motorola Edge 30 Pro',
osVersion: 'Android 13',
screen: '1080x2400 2.5x',
timezone: 'America/Sao_Paulo',
locale: 'pt-BR',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 14; SM-A546E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36',
deviceName: 'Samsung Galaxy A54',
osVersion: 'Android 14',
screen: '1080x2340 2.5x',
timezone: 'Australia/Sydney',
locale: 'en-AU',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 12; 2201116SG) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
deviceName: 'Redmi Note 11 Pro',
osVersion: 'Android 12',
screen: '1080x2400 2.6x',
timezone: 'Europe/Rome',
locale: 'it-IT',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 13; ZS676KS) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36',
deviceName: 'Asus ROG Phone 6',
osVersion: 'Android 13',
screen: '1080x2448 2.6x',
timezone: 'Asia/Taipei',
locale: 'zh-TW',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 10; TA-1021) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Mobile Safari/537.36',
deviceName: 'Nokia 8',
osVersion: 'Android 10',
screen: '1440x2560 2.4x',
timezone: 'Europe/Helsinki',
locale: 'fi-FI',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 13; PGT-N19) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36',
deviceName: 'Huawei P60 Pro',
osVersion: 'Android 13 (EMUI)',
screen: '1220x2700 2.7x',
timezone: 'Europe/Paris',
locale: 'fr-FR',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 9; LM-G710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Mobile Safari/537.36',
deviceName: 'LG G7 ThinQ',
osVersion: 'Android 9',
screen: '1440x3120 2.8x',
timezone: 'Asia/Seoul',
locale: 'ko-KR',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 14; Nothing Phone (2)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36',
deviceName: 'Nothing Phone (2)',
osVersion: 'Android 14',
screen: '1080x2412 2.5x',
timezone: 'America/Toronto',
locale: 'en-CA',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 13; SM-F936U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36',
deviceName: 'Samsung Galaxy Z Fold 4',
osVersion: 'Android 13',
screen: '1812x2176 2.2x',
timezone: 'America/Denver',
locale: 'en-US',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 12; LE2113) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36',
deviceName: 'OnePlus 9',
osVersion: 'Android 12',
screen: '1080x2400 2.6x',
timezone: 'Europe/Stockholm',
locale: 'sv-SE',
),
DevicePreset(
deviceType: 'ANDROID',
userAgent:
'Mozilla/5.0 (Linux; Android 14; Pixel 7a) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36',
deviceName: 'Google Pixel 7a',
osVersion: 'Android 14',
screen: '1080x2400 2.5x',
timezone: 'Europe/Amsterdam',
locale: 'nl-NL',
),
DevicePreset(
deviceType: 'IOS',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1',
deviceName: 'iPhone 15 Pro Max',
osVersion: 'iOS 17.5.1',
screen: '1290x2796 3.0x',
timezone: 'America/Los_Angeles',
locale: 'en-US',
),
DevicePreset(
deviceType: 'IOS',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1',
deviceName: 'iPhone 13',
osVersion: 'iOS 16.7',
screen: '1170x2532 3.0x',
timezone: 'Europe/London',
locale: 'en-GB',
),
DevicePreset(
deviceType: 'IOS',
userAgent:
'Mozilla/5.0 (iPad; CPU OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/124.0.6367.88 Mobile/15E148 Safari/604.1',
deviceName: 'iPad Pro 11-inch',
osVersion: 'iPadOS 17.5',
screen: '1668x2388 2.0x',
timezone: 'Europe/Paris',
locale: 'fr-FR',
),
DevicePreset(
deviceType: 'IOS',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/125.0 Mobile/15E148',
deviceName: 'iPhone 14 Pro',
osVersion: 'iOS 17.4.1',
screen: '1179x2556 3.0x',
timezone: 'Europe/Berlin',
locale: 'de-DE',
),
DevicePreset(
deviceType: 'IOS',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 15_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.3 Mobile/15E148 Safari/604.1',
deviceName: 'iPhone SE (2020)',
osVersion: 'iOS 15.8',
screen: '750x1334 2.0x',
timezone: 'Australia/Melbourne',
locale: 'en-AU',
),
DevicePreset(
deviceType: 'IOS',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) OPR/55.0.2519.144889 Mobile/15E148',
deviceName: 'iPhone 15',
osVersion: 'iOS 17.1',
screen: '1179x2556 3.0x',
timezone: 'Asia/Tokyo',
locale: 'ja-JP',
),
DevicePreset(
deviceType: 'IOS',
userAgent:
'Mozilla/5.0 (iPad; CPU OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1',
deviceName: 'iPad Air 5th Gen',
osVersion: 'iPadOS 16.5',
screen: '1640x2360 2.0x',
timezone: 'America/Toronto',
locale: 'en-CA',
),
DevicePreset(
deviceType: 'DESKTOP',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
deviceName: 'Windows PC',
osVersion: 'Windows 11',
screen: '1920x1080 1.25x',
timezone: 'Europe/Moscow',
locale: 'ru-RU',
),
DevicePreset(
deviceType: 'DESKTOP',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
deviceName: 'MacBook Pro',
osVersion: 'macOS 14.5 Sonoma',
screen: '1728x1117 2.0x',
timezone: 'America/New_York',
locale: 'en-US',
),
DevicePreset(
deviceType: 'DESKTOP',
userAgent:
'Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0',
deviceName: 'Linux PC',
osVersion: 'Ubuntu 24.04 LTS',
screen: '2560x1440 1.0x',
timezone: 'UTC',
locale: 'en-GB',
),
DevicePreset(
deviceType: 'DESKTOP',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0',
deviceName: 'Windows PC (Firefox)',
osVersion: 'Windows 10',
screen: '1536x864 1.0x',
timezone: 'Europe/Paris',
locale: 'fr-FR',
),
DevicePreset(
deviceType: 'DESKTOP',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15',
deviceName: 'iMac (Safari)',
osVersion: 'macOS 13.6 Ventura',
screen: '3840x2160 1.5x',
timezone: 'America/Los_Angeles',
locale: 'en-US',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
deviceName: 'Chrome',
osVersion: 'Windows ',
screen: '1920x1080',
timezone: 'Europe/Berlin',
locale: 'de-DE',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
deviceName: 'Chrome',
osVersion: 'Windows',
screen: '2560x1440',
timezone: 'America/New_York',
locale: 'en-US',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.2420.97',
deviceName: 'Edge',
osVersion: 'Windows',
screen: '1536x864',
timezone: 'Europe/London',
locale: 'en-GB',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
deviceName: 'Chrome',
osVersion: 'Windows',
screen: '1920x1200',
timezone: 'Europe/Paris',
locale: 'fr-FR',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
deviceName: 'Chrome',
osVersion: 'Windows',
screen: '1366x768',
timezone: 'Europe/Madrid',
locale: 'es-ES',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0',
deviceName: 'Firefox',
osVersion: 'Windows',
screen: '1920x1080',
timezone: 'Europe/Rome',
locale: 'it-IT',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0',
deviceName: 'Firefox',
osVersion: 'Windows',
screen: '1440x900',
timezone: 'Europe/Amsterdam',
locale: 'nl-NL',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
deviceName: 'Firefox',
osVersion: 'Windows',
screen: '1600x900',
timezone: 'Europe/Warsaw',
locale: 'pl-PL',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.2535.51',
deviceName: 'Edge',
osVersion: 'Windows',
screen: '1920x1080',
timezone: 'America/Chicago',
locale: 'en-US',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.2478.109',
deviceName: 'Edge',
osVersion: 'Windows',
screen: '1366x768',
timezone: 'America/Sao_Paulo',
locale: 'pt-BR',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
deviceName: 'Chrome',
osVersion: 'macOS 14.5',
screen: '2560x1440',
timezone: 'America/Los_Angeles',
locale: 'en-US',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
deviceName: 'Chrome',
osVersion: 'macOS 13.6',
screen: '1440x900',
timezone: 'America/Toronto',
locale: 'en-CA',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_7_10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
deviceName: 'Chrome',
osVersion: 'macOS 11.7',
screen: '1728x1117',
timezone: 'Australia/Sydney',
locale: 'en-AU',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
deviceName: 'Chrome',
osVersion: 'macOS 12.5',
screen: '2048x1152',
timezone: 'Europe/London',
locale: 'en-GB',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0',
deviceName: 'Firefox',
osVersion: 'macOS 14.5',
screen: '1920x1080',
timezone: 'America/New_York',
locale: 'en-US',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:125.0) Gecko/20100101 Firefox/125.0',
deviceName: 'Firefox',
osVersion: 'macOS 13.0',
screen: '1680x1050',
timezone: 'Europe/Berlin',
locale: 'de-DE',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15',
deviceName: 'Safari',
osVersion: 'macOS 14.5',
screen: '1440x900',
timezone: 'America/New_York',
locale: 'en-US',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15',
deviceName: 'Safari',
osVersion: 'macOS 13.6',
screen: '2560x1600',
timezone: 'Europe/Paris',
locale: 'fr-FR',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.1 Safari/605.1.15',
deviceName: 'Safari',
osVersion: 'macOS 10.14',
screen: '1280x800',
timezone: 'Asia/Tokyo',
locale: 'ja-JP',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
deviceName: 'Chrome',
osVersion: 'Linux',
screen: '1920x1080',
timezone: 'Europe/Moscow',
locale: 'ru-RU',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
deviceName: 'Chrome',
osVersion: 'Linux',
screen: '1366x768',
timezone: 'Asia/Kolkata',
locale: 'en-IN',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
deviceName: 'Chrome',
osVersion: 'Chrome OS',
screen: '1920x1080',
timezone: 'America/Mexico_City',
locale: 'es-MX',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
deviceName: 'Chrome',
osVersion: 'Linux',
screen: '1600x900',
timezone: 'Asia/Shanghai',
locale: 'zh-CN',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0',
deviceName: 'Firefox',
osVersion: 'Linux',
screen: '1920x1080',
timezone: 'UTC',
locale: 'en-GB',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0',
deviceName: 'Firefox',
osVersion: 'Linux',
screen: '2560x1440',
timezone: 'America/Denver',
locale: 'en-US',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
deviceName: 'Firefox',
osVersion: 'Linux',
screen: '1366x768',
timezone: 'Asia/Dubai',
locale: 'ar-AE',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 OPR/110.0.0.0',
deviceName: 'Opera',
osVersion: 'Windows',
screen: '1920x1080',
timezone: 'Europe/Oslo',
locale: 'no-NO',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Vivaldi/6.5.3206.63',
deviceName: 'Vivaldi',
osVersion: 'macOS 14.0',
screen: '1440x900',
timezone: 'Europe/Stockholm',
locale: 'sv-SE',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0',
deviceName: 'Firefox',
osVersion: 'Windows',
screen: '1280x720',
timezone: 'Asia/Seoul',
locale: 'ko-KR',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
deviceName: 'Chrome',
osVersion: 'Linux',
screen: '1920x1080',
timezone: 'Europe/Helsinki',
locale: 'fi-FI',
),
DevicePreset(
deviceType: 'WEB',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15',
deviceName: 'Safari',
osVersion: 'macOS 10.13',
screen: '1280x800',
timezone: 'America/Vancouver',
locale: 'en-CA',
),
];

View File

@@ -0,0 +1,935 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:video_player/video_player.dart';
import 'dart:async';
class FullScreenVideoPlayer extends StatefulWidget {
final String videoUrl;
const FullScreenVideoPlayer({Key? key, required this.videoUrl})
: super(key: key);
@override
State<FullScreenVideoPlayer> createState() => _FullScreenVideoPlayerState();
}
class _FullScreenVideoPlayerState extends State<FullScreenVideoPlayer>
with SingleTickerProviderStateMixin {
VideoPlayerController? _videoPlayerController;
bool _isLoading = true;
bool _hasError = false;
bool _isPlaying = false;
bool _showControls = true;
bool _isBuffering = false;
double _playbackSpeed = 1.0;
Timer? _hideControlsTimer;
Timer? _positionTimer;
late AnimationController _controlsAnimationController;
late Animation<double> _controlsAnimation;
bool _isDragging = false;
Duration _currentPosition = Duration.zero;
Duration _totalDuration = Duration.zero;
List<DurationRange> _bufferedRanges = [];
@override
void initState() {
super.initState();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
DeviceOrientation.portraitUp,
]);
_controlsAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_controlsAnimation = CurvedAnimation(
parent: _controlsAnimationController,
curve: Curves.easeInOut,
);
_controlsAnimationController.forward();
_initializePlayer();
}
Future<void> _initializePlayer() async {
try {
_videoPlayerController = VideoPlayerController.networkUrl(
Uri.parse(widget.videoUrl),
httpHeaders: const {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
},
);
_videoPlayerController!.addListener(_videoListener);
await _videoPlayerController!.initialize();
_videoPlayerController!.play();
if (mounted) {
setState(() {
_isLoading = false;
_isPlaying = true;
_totalDuration = _videoPlayerController!.value.duration;
_currentPosition = _videoPlayerController!.value.position;
});
_startHideControlsTimer();
_startPositionTimer();
}
} catch (e) {
print('❌ [FullScreenVideoPlayer] Error initializing player: $e');
if (mounted) {
setState(() {
_hasError = true;
_isLoading = false;
});
}
}
}
void _videoListener() {
if (!mounted) return;
final controller = _videoPlayerController!;
setState(() {
_isPlaying = controller.value.isPlaying;
_isBuffering = controller.value.isBuffering;
_totalDuration = controller.value.duration;
_bufferedRanges = controller.value.buffered;
if (!_isDragging) {
_currentPosition = controller.value.position;
}
});
}
void _startHideControlsTimer() {
_hideControlsTimer?.cancel();
_hideControlsTimer = Timer(const Duration(seconds: 3), () {
if (_isPlaying && !_isDragging) {
_hideControlsUI();
}
});
}
void _startPositionTimer() {
_positionTimer?.cancel();
_positionTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
if (!mounted || _isDragging) return;
if (_videoPlayerController != null &&
_videoPlayerController!.value.isInitialized) {
setState(() {
_currentPosition = _videoPlayerController!.value.position;
});
}
});
}
void _showControlsUI() {
if (_showControls) return;
setState(() {
_showControls = true;
});
_controlsAnimationController.forward();
_startHideControlsTimer();
}
void _hideControlsUI() {
if (!_showControls) return;
setState(() {
_showControls = false;
});
_controlsAnimationController.reverse();
}
void _togglePlayPause() {
setState(() {
if (_isPlaying) {
_videoPlayerController!.pause();
_showControlsUI();
} else {
_videoPlayerController!.play();
_startHideControlsTimer();
}
});
}
Duration _clampDuration(Duration value, Duration min, Duration max) {
if (value < min) return min;
if (value > max) return max;
return value;
}
void _seekTo(Duration position) {
_videoPlayerController!.seekTo(position);
setState(() {
_currentPosition = position;
_isDragging = false;
});
_startHideControlsTimer();
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final seconds = duration.inSeconds.remainder(60);
if (hours > 0) {
return '${twoDigits(hours)}:${twoDigits(minutes)}:${twoDigits(seconds)}';
}
return '${twoDigits(minutes)}:${twoDigits(seconds)}';
}
void _showSpeedMenu() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => _SpeedBottomSheet(
currentSpeed: _playbackSpeed,
onSpeedSelected: (speed) {
setState(() {
_playbackSpeed = speed;
_videoPlayerController!.setPlaybackSpeed(speed);
});
Navigator.pop(context);
_showControlsUI();
},
),
);
}
@override
void dispose() {
_hideControlsTimer?.cancel();
_positionTimer?.cancel();
_videoPlayerController?.removeListener(_videoListener);
_videoPlayerController?.dispose();
_controlsAnimationController.dispose();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
if (_showControls) {
_hideControlsUI();
} else {
_showControlsUI();
}
},
onDoubleTapDown: (details) {
final screenWidth = MediaQuery.of(context).size.width;
if (details.globalPosition.dx < screenWidth / 2) {
final newPosition = _clampDuration(
_currentPosition - const Duration(seconds: 10),
Duration.zero,
_totalDuration,
);
_seekTo(newPosition);
_showControlsUI();
} else {
final newPosition = _clampDuration(
_currentPosition + const Duration(seconds: 10),
Duration.zero,
_totalDuration,
);
_seekTo(newPosition);
_showControlsUI();
}
},
child: Stack(
children: [
Center(
child: _isLoading
? CircularProgressIndicator(
color: colorScheme.primary,
)
: _hasError
? _ErrorWidget(colorScheme: colorScheme)
: _videoPlayerController != null &&
_videoPlayerController!.value.isInitialized
? AspectRatio(
aspectRatio: _videoPlayerController!.value.aspectRatio,
child: VideoPlayer(_videoPlayerController!),
)
: const SizedBox(),
),
if (_isBuffering)
Center(
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
color: colorScheme.primary,
strokeWidth: 3,
),
const SizedBox(height: 12),
Text(
'Буферизация...',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
],
),
),
if (_showControls)
GestureDetector(
onDoubleTapDown: (details) {
final screenWidth = MediaQuery.of(context).size.width;
if (details.globalPosition.dx < screenWidth / 2) {
final newPosition = _clampDuration(
_currentPosition - const Duration(seconds: 10),
Duration.zero,
_totalDuration,
);
_seekTo(newPosition);
_showControlsUI();
} else {
final newPosition = _clampDuration(
_currentPosition + const Duration(seconds: 10),
Duration.zero,
_totalDuration,
);
_seekTo(newPosition);
_showControlsUI();
}
},
behavior: HitTestBehavior.translucent,
child: AnimatedBuilder(
animation: _controlsAnimation,
builder: (context, child) {
return Opacity(
opacity: _controlsAnimation.value,
child: child,
);
},
child: _VideoControls(
colorScheme: colorScheme,
isPlaying: _isPlaying,
currentPosition: _currentPosition,
totalDuration: _totalDuration,
bufferedRanges: _bufferedRanges,
playbackSpeed: _playbackSpeed,
onPlayPause: _togglePlayPause,
onSeek: (position) {
setState(() {
_isDragging = true;
_currentPosition = position;
});
},
onSeekEnd: (position) {
_seekTo(position);
},
onBack: () => Navigator.pop(context),
onSpeedTap: () {
_showSpeedMenu();
},
onRewind: () {
final newPosition = _clampDuration(
_currentPosition - const Duration(seconds: 10),
Duration.zero,
_totalDuration,
);
_seekTo(newPosition);
_showControlsUI();
},
onForward: () {
final newPosition = _clampDuration(
_currentPosition + const Duration(seconds: 10),
Duration.zero,
_totalDuration,
);
_seekTo(newPosition);
_showControlsUI();
},
formatDuration: _formatDuration,
),
),
),
],
),
);
}
}
class _VideoControls extends StatelessWidget {
final ColorScheme colorScheme;
final bool isPlaying;
final Duration currentPosition;
final Duration totalDuration;
final List<DurationRange> bufferedRanges;
final double playbackSpeed;
final VoidCallback onPlayPause;
final Function(Duration) onSeek;
final Function(Duration) onSeekEnd;
final VoidCallback onBack;
final VoidCallback onSpeedTap;
final VoidCallback onRewind;
final VoidCallback onForward;
final String Function(Duration) formatDuration;
const _VideoControls({
required this.colorScheme,
required this.isPlaying,
required this.currentPosition,
required this.totalDuration,
required this.bufferedRanges,
required this.playbackSpeed,
required this.onPlayPause,
required this.onSeek,
required this.onSeekEnd,
required this.onBack,
required this.onSpeedTap,
required this.onRewind,
required this.onForward,
required this.formatDuration,
});
@override
Widget build(BuildContext context) {
final progress = totalDuration.inMilliseconds > 0
? currentPosition.inMilliseconds / totalDuration.inMilliseconds
: 0.0;
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent,
Colors.transparent,
Colors.black.withOpacity(0.7),
],
),
),
child: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
IconButton(
onPressed: onBack,
icon: const Icon(Icons.arrow_back),
style: IconButton.styleFrom(
backgroundColor: Colors.black.withOpacity(0.5),
foregroundColor: Colors.white,
shape: const CircleBorder(),
),
),
const Spacer(),
FilledButton.tonal(
onPressed: onSpeedTap,
style: FilledButton.styleFrom(
backgroundColor: Colors.black.withOpacity(0.5),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.speed, size: 18),
const SizedBox(width: 6),
Text(
'${playbackSpeed}x',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
const Spacer(),
// Прогресс-бар
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
_CustomProgressBar(
progress: progress,
currentPosition: currentPosition,
totalDuration: totalDuration,
bufferedRanges: bufferedRanges,
onSeek: onSeek,
onSeekEnd: onSeekEnd,
colorScheme: colorScheme,
formatDuration: formatDuration,
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
formatDuration(currentPosition),
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const Text(
' / ',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
Text(
formatDuration(totalDuration),
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_MaterialYouControlButton(
icon: Icons.replay_10,
onTap: onRewind,
colorScheme: colorScheme,
label: '-10',
),
const SizedBox(width: 12),
_MaterialYouControlButton(
icon: isPlaying ? Icons.pause : Icons.play_arrow,
onTap: onPlayPause,
colorScheme: colorScheme,
isPrimary: true,
),
const SizedBox(width: 12),
// Кнопка перемотки вперед
_MaterialYouControlButton(
icon: Icons.forward_10,
onTap: onForward,
colorScheme: colorScheme,
label: '+10',
),
],
),
const SizedBox(height: 24),
],
),
),
],
),
),
);
}
}
class _MaterialYouControlButton extends StatelessWidget {
final IconData icon;
final VoidCallback onTap;
final ColorScheme colorScheme;
final String? label;
final bool isPrimary;
const _MaterialYouControlButton({
required this.icon,
required this.onTap,
required this.colorScheme,
this.label,
this.isPrimary = false,
});
@override
Widget build(BuildContext context) {
if (isPrimary) {
return FilledButton(
onPressed: onTap,
style: FilledButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
padding: const EdgeInsets.all(20),
shape: const CircleBorder(),
minimumSize: const Size(72, 72),
elevation: 3,
),
child: Icon(icon, size: 36),
);
} else {
return FilledButton.tonal(
onPressed: onTap,
style: FilledButton.styleFrom(
backgroundColor: Colors.white.withOpacity(0.16),
foregroundColor: Colors.white,
padding: const EdgeInsets.all(14),
shape: const CircleBorder(),
minimumSize: const Size(60, 60),
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 26),
if (label != null) ...[
const SizedBox(height: 2),
Text(
label!,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w600,
height: 1.0,
),
),
],
],
),
);
}
}
}
class _CustomProgressBar extends StatefulWidget {
final double progress;
final Duration currentPosition;
final Duration totalDuration;
final List<DurationRange> bufferedRanges;
final Function(Duration) onSeek;
final Function(Duration) onSeekEnd;
final ColorScheme colorScheme;
final String Function(Duration) formatDuration;
const _CustomProgressBar({
required this.progress,
required this.currentPosition,
required this.totalDuration,
required this.bufferedRanges,
required this.onSeek,
required this.onSeekEnd,
required this.colorScheme,
required this.formatDuration,
});
@override
State<_CustomProgressBar> createState() => _CustomProgressBarState();
}
class _CustomProgressBarState extends State<_CustomProgressBar> {
bool _isDragging = false;
double _dragProgress = 0.0;
Duration _getPositionFromLocalPosition(Offset localPosition, Size size) {
final progress = (localPosition.dx / size.width).clamp(0.0, 1.0);
return Duration(
milliseconds: (progress * widget.totalDuration.inMilliseconds).round(),
);
}
@override
Widget build(BuildContext context) {
final progress = _isDragging ? _dragProgress : widget.progress;
final currentPos = Duration(
milliseconds: (progress * widget.totalDuration.inMilliseconds).round(),
);
return GestureDetector(
onPanStart: (details) {
setState(() {
_isDragging = true;
});
final box = context.findRenderObject() as RenderBox;
final localPosition = box.globalToLocal(details.globalPosition);
_dragProgress = (localPosition.dx / box.size.width).clamp(0.0, 1.0);
final position = _getPositionFromLocalPosition(localPosition, box.size);
widget.onSeek(position);
},
onPanUpdate: (details) {
final box = context.findRenderObject() as RenderBox;
final localPosition = box.globalToLocal(details.globalPosition);
setState(() {
_dragProgress = (localPosition.dx / box.size.width).clamp(0.0, 1.0);
});
final position = _getPositionFromLocalPosition(localPosition, box.size);
widget.onSeek(position);
},
onPanEnd: (details) {
setState(() {
_isDragging = false;
});
widget.onSeekEnd(currentPos);
},
onTapDown: (details) {
if (_isDragging) return;
final box = context.findRenderObject() as RenderBox;
final localPosition = box.globalToLocal(details.globalPosition);
final position = _getPositionFromLocalPosition(localPosition, box.size);
widget.onSeekEnd(position);
},
child: Container(
height: 48,
child: LayoutBuilder(
builder: (context, constraints) {
final containerWidth = constraints.maxWidth;
return Stack(
children: [
Center(
child: Container(
height: 4,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(2),
),
),
),
if (widget.totalDuration.inMilliseconds > 0)
...widget.bufferedRanges.map((range) {
final startProgress = (range.start.inMilliseconds / widget.totalDuration.inMilliseconds).clamp(0.0, 1.0);
final endProgress = (range.end.inMilliseconds / widget.totalDuration.inMilliseconds).clamp(0.0, 1.0);
final bufferedWidth = (endProgress - startProgress).clamp(0.0, 1.0);
if (bufferedWidth <= 0) return const SizedBox.shrink();
final leftOffset = startProgress * containerWidth;
final bufferedWidthPx = bufferedWidth * containerWidth;
return Positioned(
left: leftOffset,
top: 22,
child: Container(
width: bufferedWidthPx,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.5),
borderRadius: BorderRadius.circular(2),
),
),
);
}).toList(),
Center(
child: Align(
alignment: Alignment.centerLeft,
child: FractionallySizedBox(
widthFactor: progress,
child: Container(
height: 4,
decoration: BoxDecoration(
color: widget.colorScheme.primary,
borderRadius: BorderRadius.circular(2),
),
),
),
),
),
// Ползунок
Center(
child: Align(
alignment: Alignment(progress * 2 - 1, 0),
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: widget.colorScheme.primary,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
),
),
),
],
);
},
),
),
);
}
}
class _SpeedBottomSheet extends StatelessWidget {
final double currentSpeed;
final Function(double) onSpeedSelected;
const _SpeedBottomSheet({
required this.currentSpeed,
required this.onSpeedSelected,
});
@override
Widget build(BuildContext context) {
final speeds = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0];
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Container(
decoration: BoxDecoration(
color: theme.dialogBackgroundColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
),
padding: const EdgeInsets.symmetric(vertical: 24),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(2),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
Text(
'Скорость воспроизведения',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.center,
children: speeds.map((speed) {
final isSelected = speed == currentSpeed;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => onSpeedSelected(speed),
borderRadius: BorderRadius.circular(20),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
decoration: BoxDecoration(
color: isSelected
? colorScheme.primaryContainer
: colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(20),
),
child: Text(
'${speed}x',
style: TextStyle(
color: isSelected
? colorScheme.onPrimaryContainer
: colorScheme.onSurfaceVariant,
fontSize: 16,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
),
),
);
}).toList(),
),
const SizedBox(height: 24),
],
),
),
);
}
}
class _ErrorWidget extends StatelessWidget {
final ColorScheme colorScheme;
const _ErrorWidget({required this.colorScheme});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: colorScheme.errorContainer,
shape: BoxShape.circle,
),
child: Icon(
Icons.error_outline,
color: colorScheme.onErrorContainer,
size: 48,
),
),
const SizedBox(height: 24),
Text(
'Не удалось загрузить видео',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
'Проверьте подключение к интернету\nили попробуйте позже',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,289 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:http/http.dart' as http;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:es_compression/lz4.dart';
class ImageCacheService {
ImageCacheService._privateConstructor();
static final ImageCacheService instance =
ImageCacheService._privateConstructor();
static const String _cacheDirectoryName = 'image_cache';
static const Duration _cacheExpiration = Duration(
days: 7,
); // Кеш изображений на 7 дней
late Directory _cacheDirectory;
// LZ4 сжатие для экономии места
final Lz4Codec _lz4Codec = Lz4Codec();
Future<void> initialize() async {
final appDir = await getApplicationDocumentsDirectory();
_cacheDirectory = Directory(path.join(appDir.path, _cacheDirectoryName));
if (!_cacheDirectory.existsSync()) {
await _cacheDirectory.create(recursive: true);
}
await _cleanupExpiredCache();
}
String getCachedImagePath(String url) {
final fileName = _generateFileName(url);
return path.join(_cacheDirectory.path, fileName);
}
bool isImageCached(String url) {
final file = File(getCachedImagePath(url));
return file.existsSync();
}
Future<File?> loadImage(String url, {bool forceRefresh = false}) async {
if (!forceRefresh && isImageCached(url)) {
final cachedFile = File(getCachedImagePath(url));
if (await _isFileValid(cachedFile)) {
return cachedFile;
} else {
await cachedFile.delete();
}
}
try {
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final file = File(getCachedImagePath(url));
// Сжимаем данные перед сохранением
final compressedData = _lz4Codec.encode(response.bodyBytes);
await file.writeAsBytes(compressedData);
await _updateFileAccessTime(file);
return file;
}
} catch (e) {
print('Ошибка загрузки изображения $url: $e');
}
return null;
}
Future<Uint8List?> loadImageAsBytes(
String url, {
bool forceRefresh = false,
}) async {
final file = await loadImage(url, forceRefresh: forceRefresh);
if (file != null) {
final compressedData = await file.readAsBytes();
try {
// Декомпрессируем данные
final decompressedData = _lz4Codec.decode(compressedData);
return Uint8List.fromList(decompressedData);
} catch (e) {
// Если декомпрессия не удалась, возможно файл не сжат (старый формат)
print(
'Ошибка декомпрессии изображения $url, пробуем прочитать как обычный файл: $e',
);
return compressedData;
}
}
return null;
}
Future<void> preloadImage(String url) async {
await loadImage(url);
}
Future<void> preloadContactAvatar(String? photoUrl) async {
if (photoUrl != null && photoUrl.isNotEmpty) {
await preloadImage(photoUrl);
}
}
Future<void> preloadProfileAvatar(String? photoUrl) async {
if (photoUrl != null && photoUrl.isNotEmpty) {
await preloadImage(photoUrl);
}
}
Future<void> preloadContactAvatars(List<String?> photoUrls) async {
final futures = photoUrls
.where((url) => url != null && url.isNotEmpty)
.map((url) => preloadImage(url!))
.toList();
if (futures.isNotEmpty) {
await Future.wait(futures);
}
}
Future<void> clearCache() async {
if (_cacheDirectory.existsSync()) {
await _clearDirectoryContents(_cacheDirectory);
}
}
Future<void> _clearDirectoryContents(Directory directory) async {
try {
// Очищаем содержимое директории, удаляя файлы по одному
await for (final entity in directory.list(recursive: true)) {
if (entity is File) {
try {
await entity.delete();
// Небольшая задержка между удалениями для избежания конфликтов
await Future.delayed(const Duration(milliseconds: 5));
} catch (fileError) {
// Игнорируем ошибки удаления отдельных файлов
print('Не удалось удалить файл ${entity.path}: $fileError');
}
} else if (entity is Directory) {
try {
// Рекурсивно очищаем поддиректории
await _clearDirectoryContents(entity);
try {
await entity.delete();
} catch (dirError) {
print(
'Не удалось удалить поддиректорию ${entity.path}: $dirError',
);
}
} catch (subDirError) {
print('Ошибка очистки поддиректории ${entity.path}: $subDirError');
}
}
}
print('Содержимое директории ${directory.path} очищено');
} catch (e) {
print('Ошибка очистки содержимого директории ${directory.path}: $e');
}
}
Future<int> getCacheSize() async {
int totalSize = 0;
if (_cacheDirectory.existsSync()) {
await for (final entity in _cacheDirectory.list(recursive: true)) {
if (entity is File) {
totalSize += await entity.length();
}
}
}
return totalSize;
}
Future<int> getCacheFileCount() async {
int count = 0;
if (_cacheDirectory.existsSync()) {
await for (final entity in _cacheDirectory.list(recursive: true)) {
if (entity is File) {
count++;
}
}
}
return count;
}
Future<void> _cleanupExpiredCache() async {
if (!_cacheDirectory.existsSync()) return;
await for (final entity in _cacheDirectory.list(recursive: true)) {
if (entity is File && await _isFileExpired(entity)) {
await entity.delete();
}
}
}
Future<bool> _isFileValid(File file) async {
if (!file.existsSync()) return false;
final stat = await file.stat();
final age = DateTime.now().difference(stat.modified);
return age < _cacheExpiration;
}
Future<bool> _isFileExpired(File file) async {
if (!file.existsSync()) return false;
final stat = await file.stat();
final age = DateTime.now().difference(stat.modified);
return age >= _cacheExpiration;
}
Future<void> _updateFileAccessTime(File file) async {
try {
await file.setLastModified(DateTime.now());
} catch (e) {}
}
String _generateFileName(String url) {
final hash = url.hashCode.abs().toString();
final extension = path.extension(url).isNotEmpty
? path.extension(url)
: '.jpg'; // По умолчанию jpg
return '$hash$extension';
}
Future<Map<String, dynamic>> getCacheStats() async {
final size = await getCacheSize();
final fileCount = await getCacheFileCount();
return {
'cache_size_bytes': size,
'cache_size_mb': (size / (1024 * 1024)).toStringAsFixed(2),
'file_count': fileCount,
'cache_directory': _cacheDirectory.path,
'compression_enabled': true,
'compression_algorithm': 'LZ4',
};
}
}
extension CachedImageExtension on String {
Widget getCachedNetworkImage({
Key? key,
double? width,
double? height,
BoxFit? fit,
Widget? placeholder,
Widget? errorWidget,
Duration? fadeInDuration,
bool useMemoryCache = true,
}) {
return CachedNetworkImage(
key: key,
imageUrl: this,
width: width,
height: height,
fit: fit,
placeholder: (context, url) =>
placeholder ??
Container(
width: width,
height: height,
color: Colors.grey[300],
child: const Icon(Icons.image, color: Colors.grey),
),
errorWidget: (context, url, error) =>
errorWidget ??
Container(
width: width,
height: height,
color: Colors.grey[300],
child: const Icon(Icons.broken_image, color: Colors.grey),
),
fadeInDuration: fadeInDuration ?? const Duration(milliseconds: 300),
useOldImageOnUrlChange: true,
memCacheWidth: useMemoryCache ? (width ?? 200).toInt() : null,
memCacheHeight: useMemoryCache ? (height ?? 200).toInt() : null,
);
}
Future<void> preloadImage() async {
await ImageCacheService.instance.loadImage(this);
}
}

View File

@@ -0,0 +1,98 @@
import 'dart:typed_data';
import 'package:msgpack_dart/msgpack_dart.dart';
import 'package:es_compression/lz4.dart';
final lz4Codec = Lz4Codec();
Uint8List packPacket({
required int ver,
required int cmd,
required int seq,
required int opcode,
required Map<String, dynamic> payload,
}) {
Uint8List payloadBytes = serialize(payload);
bool isCompressed = false;
if (payloadBytes.length >= 32) {
final uncompressedSize = ByteData(4)
..setUint32(0, payloadBytes.length, Endian.big);
final compressedData = lz4Codec.encode(payloadBytes);
final builder = BytesBuilder();
builder.add(uncompressedSize.buffer.asUint8List());
builder.add(compressedData);
payloadBytes = builder.toBytes();
isCompressed = true;
}
final header = ByteData(10);
header.setUint8(0, ver);
header.setUint16(1, cmd, Endian.big);
header.setUint8(3, seq);
header.setUint16(4, opcode, Endian.big);
int packedLen = payloadBytes.length;
if (isCompressed) {
packedLen |= (1 << 24);
}
header.setUint32(6, packedLen, Endian.big);
final builder = BytesBuilder();
builder.add(header.buffer.asUint8List());
builder.add(payloadBytes);
return builder.toBytes();
}
Map<String, dynamic>? unpackPacket(Uint8List data) {
if (data.length < 10) {
print("Ошибка распаковки: Пакет слишком короткий для заголовка.");
return null;
}
final byteData = data.buffer.asByteData(
data.offsetInBytes,
data.lengthInBytes,
);
final ver = byteData.getUint8(0);
final cmd = byteData.getUint16(1, Endian.big);
final seq = byteData.getUint8(3);
final opcode = byteData.getUint16(4, Endian.big);
final packedLen = byteData.getUint32(6, Endian.big);
final compFlag = packedLen >> 24;
final payloadLength = packedLen & 0x00FFFFFF;
if (data.length < 10 + payloadLength) {
print(
"Ошибка распаковки: Фактическая длина пакета (${data.length}) меньше заявленной (${10 + payloadLength}).",
);
return null;
}
Uint8List payloadBytes = data.sublist(10, 10 + payloadLength);
if (compFlag != 0) {
try {
final compressedData = payloadBytes.sublist(4);
payloadBytes = Uint8List.fromList(lz4Codec.decode(compressedData));
} catch (e) {
print("Ошибка распаковки LZ4: $e");
return null;
}
}
final dynamic payload = deserialize(payloadBytes);
return {
"ver": ver,
"cmd": cmd,
"seq": seq,
"opcode": opcode,
"payload": payload,
};
}

View File

@@ -0,0 +1,158 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:shared_preferences/shared_preferences.dart';
import 'proxy_settings.dart';
class ProxyService {
ProxyService._privateConstructor();
static final ProxyService instance = ProxyService._privateConstructor();
static const _proxySettingsKey = 'proxy_settings';
Future<void> saveProxySettings(ProxySettings settings) async {
final prefs = await SharedPreferences.getInstance();
final jsonString = jsonEncode(settings.toJson());
await prefs.setString(_proxySettingsKey, jsonString);
}
Future<ProxySettings> loadProxySettings() async {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_proxySettingsKey);
if (jsonString != null) {
try {
return ProxySettings.fromJson(jsonDecode(jsonString));
} catch (e) {
return ProxySettings();
}
}
return ProxySettings();
}
Future<void> checkProxy(ProxySettings settings) async {
print("Проверка прокси: ${settings.host}:${settings.port}");
if (settings.protocol == ProxyProtocol.socks5) {
await _checkSocks5Proxy(settings);
return;
}
HttpClient client = _createClientWithOptions(settings);
client.connectionTimeout = const Duration(seconds: 10);
try {
final request = await client.headUrl(
Uri.parse('https://www.google.com/generate_204'),
);
final response = await request.close();
print("Ответ от прокси получен, статус: ${response.statusCode}");
if (response.statusCode >= 400) {
throw Exception('Прокси вернул ошибку: ${response.statusCode}');
}
} on HandshakeException catch (e) {
print("Поймана ошибка сертификата при проверке прокси: $e");
print(
"Предполагаем, что badCertificateCallback обработает это в реальном соединении. Считаем проверку успешной.",
);
return;
} on SocketException catch (e) {
print("Ошибка сокета при проверке прокси: $e");
throw Exception('Неверный хост или порт');
} on TimeoutException catch (_) {
print("Таймаут при проверке прокси");
throw Exception('Сервер не отвечает (таймаут)');
} catch (e) {
print("Неизвестная ошибка при проверке прокси: $e");
throw Exception('Неизвестная ошибка: ${e.toString()}');
} finally {
client.close();
}
}
Future<void> _checkSocks5Proxy(ProxySettings settings) async {
Socket? proxySocket;
try {
print("Проверка SOCKS5 прокси: ${settings.host}:${settings.port}");
proxySocket = await Socket.connect(
settings.host,
settings.port,
timeout: const Duration(seconds: 10),
);
print("SOCKS5 прокси доступен: ${settings.host}:${settings.port}");
print(
"Внимание: Полная проверка SOCKS5 требует дополнительной реализации",
);
// Закрываем соединение
await proxySocket.close();
print("SOCKS5 прокси работает корректно");
} on SocketException catch (e) {
print("Ошибка сокета при проверке SOCKS5 прокси: $e");
throw Exception('Неверный хост или порт');
} on TimeoutException catch (_) {
print("Таймаут при проверке SOCKS5 прокси");
throw Exception('Сервер не отвечает (таймаут)');
} catch (e) {
print("Ошибка при проверке SOCKS5 прокси: $e");
throw Exception('Ошибка подключения: ${e.toString()}');
} finally {
await proxySocket?.close();
}
}
Future<HttpClient> getHttpClientWithProxy() async {
final settings = await loadProxySettings();
return _createClientWithOptions(settings);
}
HttpClient _createClientWithOptions(ProxySettings settings) {
final client = HttpClient();
if (settings.isEnabled && settings.host.isNotEmpty) {
if (settings.protocol == ProxyProtocol.socks5) {
print("Используется SOCKS5 прокси: ${settings.host}:${settings.port}");
print("Внимание: SOCKS5 для HTTP клиента может работать ограниченно");
client.findProxy = (uri) {
return settings.toFindProxyString();
};
} else {
print("Используется прокси: ${settings.toFindProxyString()}");
client.findProxy = (uri) {
return settings.toFindProxyString();
};
if (settings.username != null && settings.username!.isNotEmpty) {
print(
"Настраивается аутентификация на прокси для пользователя: ${settings.username}",
);
client.authenticateProxy = (host, port, scheme, realm) async {
client.addProxyCredentials(
host,
port,
realm ?? '',
HttpClientBasicCredentials(
settings.username!,
settings.password ?? '',
),
);
return true;
};
}
}
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
} else {
client.findProxy = HttpClient.findProxyFromEnvironment;
}
return client;
}
}

View File

@@ -0,0 +1,91 @@
enum ProxyProtocol { http, https, socks4, socks5 }
class ProxySettings {
final bool isEnabled;
final String host;
final int port;
final ProxyProtocol protocol;
final String? username;
final String? password;
ProxySettings({
this.isEnabled = false,
this.host = '',
this.port = 8080,
this.protocol = ProxyProtocol.http,
this.username,
this.password,
});
String toFindProxyString() {
if (!isEnabled || host.isEmpty) {
return 'DIRECT';
}
String protocolString;
switch (protocol) {
case ProxyProtocol.http:
case ProxyProtocol.https:
protocolString = 'PROXY'; // HttpClient ожидает 'PROXY' для HTTP и HTTPS
break;
case ProxyProtocol.socks4:
protocolString = 'SOCKS4';
break;
case ProxyProtocol.socks5:
protocolString = 'SOCKS5';
break;
}
return '$protocolString $host:$port';
}
ProxySettings copyWith({
bool? isEnabled,
String? host,
int? port,
ProxyProtocol? protocol,
String? username,
String? password,
}) {
return ProxySettings(
isEnabled: isEnabled ?? this.isEnabled,
host: host ?? this.host,
port: port ?? this.port,
protocol: protocol ?? this.protocol,
username: username ?? this.username,
password: password ?? this.password,
);
}
Map<String, dynamic> toJson() {
return {
'isEnabled': isEnabled,
'host': host,
'port': port,
'protocol': protocol.name,
'username': username,
'password': password,
};
}
factory ProxySettings.fromJson(Map<String, dynamic> json) {
return ProxySettings(
isEnabled: json['isEnabled'] ?? false,
host: json['host'] ?? '',
port: json['port'] ?? 8080,
protocol: ProxyProtocol.values.firstWhere(
(e) => e.name == json['protocol'],
orElse: () => ProxyProtocol.http,
),
username: json['username'],
password: json['password'],
);
}
}

View File

@@ -0,0 +1,28 @@
import 'package:shared_preferences/shared_preferences.dart';
class SpoofingService {
static Future<Map<String, dynamic>?> getSpoofedSessionData() async {
final prefs = await SharedPreferences.getInstance();
final isEnabled = prefs.getBool('spoofing_enabled') ?? false;
if (!isEnabled) {
return null; // Если подмена выключена, возвращаем null
}
return {
'user_agent': prefs.getString('spoof_useragent'),
'device_name': prefs.getString('spoof_devicename'),
'os_version': prefs.getString('spoof_osversion'),
'screen': prefs.getString('spoof_screen'),
'timezone': prefs.getString('spoof_timezone'),
'locale': prefs.getString('spoof_locale'),
'device_id': prefs.getString('spoof_deviceid'),
'device_type': prefs.getString('spoof_devicetype'),
'app_version': prefs.getString('spoof_appversion') ?? '25.10.10',
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
library;
class Platform {
static bool get isAndroid => false;
static bool get isIOS => false;
}

View File

@@ -0,0 +1,263 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/models/contact.dart';
class UserIdLookupScreen extends StatefulWidget {
const UserIdLookupScreen({super.key});
@override
State<UserIdLookupScreen> createState() => _UserIdLookupScreenState();
}
class _UserIdLookupScreenState extends State<UserIdLookupScreen> {
final TextEditingController _idController = TextEditingController();
final FocusNode _idFocusNode = FocusNode();
bool _isLoading = false;
Contact? _foundContact;
bool _searchAttempted = false;
Future<void> _searchById() async {
final String idText = _idController.text.trim();
if (idText.isEmpty) {
return;
}
final int? contactId = int.tryParse(idText);
if (contactId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Пожалуйста, введите корректный ID (только цифры)'),
),
);
return;
}
_idFocusNode.unfocus();
setState(() {
_isLoading = true;
_searchAttempted = true;
_foundContact = null;
});
try {
final List<Contact> contacts = await ApiService.instance
.fetchContactsByIds([contactId]);
if (mounted) {
setState(() {
_foundContact = contacts.isNotEmpty ? contacts.first : null;
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Ошибка при поиске: $e')));
}
}
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_idFocusNode.requestFocus();
});
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(title: const Text('Поиск по ID')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
TextField(
controller: _idController,
focusNode: _idFocusNode,
decoration: InputDecoration(
labelText: 'Введите ID пользователя',
filled: true,
fillColor: colors.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
prefixIcon: const Icon(Icons.person_search_outlined),
suffixIcon: _isLoading
? const Padding(
padding: EdgeInsets.all(12.0),
child: CircularProgressIndicator(strokeWidth: 2),
)
: IconButton(
icon: const Icon(Icons.search),
onPressed: _searchById,
),
),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onSubmitted: (_) => _searchById(),
),
const SizedBox(height: 32),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _isLoading
? const Center(
key: ValueKey('loading'),
child: CircularProgressIndicator(),
)
: _searchAttempted
? _foundContact != null
? _buildContactCard(_foundContact!, colors)
: _buildEmptyState(
key: const ValueKey('not_found'),
colors: colors,
icon: Icons.search_off_rounded,
title: 'Пользователь не найден',
subtitle:
'Аккаунт с ID "${_idController.text}" не существует или скрыт.',
)
: _buildEmptyState(
key: const ValueKey('initial'),
colors: colors,
icon: Icons.person_search_rounded,
title: 'Введите ID для поиска',
subtitle: 'Найдем пользователя в системе по его ID',
),
),
],
),
),
);
}
Widget _buildContactCard(Contact contact, ColorScheme colors) {
return Column(
key: const ValueKey('contact_card'),
children: [
CircleAvatar(
radius: 56,
backgroundColor: colors.primaryContainer,
backgroundImage: contact.photoBaseUrl != null
? NetworkImage(contact.photoBaseUrl!)
: null,
child: contact.photoBaseUrl == null
? Text(
contact.name.isNotEmpty ? contact.name[0].toUpperCase() : '?',
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
color: colors.onPrimaryContainer,
),
)
: null,
),
const SizedBox(height: 16),
Text(
contact.name,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
'ID: ${contact.id}',
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: colors.onSurfaceVariant),
),
const SizedBox(height: 24),
Container(
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
_buildInfoTile(
colors: colors,
icon: Icons.person_outlined,
title: 'Имя',
subtitle: contact.firstName,
),
const Divider(height: 1, indent: 16, endIndent: 16),
_buildInfoTile(
colors: colors,
icon: Icons.badge_outlined,
title: 'Фамилия',
subtitle: contact.lastName,
),
const Divider(height: 1, indent: 16, endIndent: 16),
_buildInfoTile(
colors: colors,
icon: Icons.notes_rounded,
title: 'Описание',
subtitle: contact.description,
),
],
),
),
],
);
}
Widget _buildInfoTile({
required ColorScheme colors,
required IconData icon,
required String title,
String? subtitle,
}) {
final bool hasData = subtitle != null && subtitle.isNotEmpty;
return ListTile(
leading: Icon(icon, color: colors.primary),
title: Text(title),
subtitle: Text(
hasData ? subtitle : '(не указано)',
style: TextStyle(
color: hasData
? colors.onSurfaceVariant
: colors.onSurfaceVariant.withOpacity(0.7),
fontStyle: hasData ? FontStyle.normal : FontStyle.italic,
),
),
);
}
Widget _buildEmptyState({
required Key key,
required ColorScheme colors,
required IconData icon,
required String title,
required String subtitle,
}) {
return Column(
key: key,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 64, color: colors.onSurfaceVariant.withOpacity(0.5)),
const SizedBox(height: 16),
Text(
title,
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(color: colors.onSurfaceVariant),
),
const SizedBox(height: 8),
Text(
subtitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colors.onSurfaceVariant.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
],
);
}
}