Compare commits

...

46 Commits

Author SHA1 Message Date
Illia Chub 39cbabe9ca Merge pull request 'Updated version number to 0.2.4' (#72) from master into fdroid
Reviewed-on: kherel/selfprivacy.org.app#72
2021-10-12 16:04:42 +03:00
Illia Chub 3c03a3dbd4 Updated version number to 0.2.4 2021-10-12 16:04:18 +03:00
Kherel 46efb6ac94 Merge branch 'master' into fdroid 2021-10-12 14:59:26 +02:00
Kherel c9aa513e29 merge 2021-10-12 14:10:00 +02:00
Kherel 2eab3fb8e4 fix conflict 2021-10-12 14:05:24 +02:00
kherel bdf5a3b1cc Merge pull request 'no-hash' (#70) from no-hash into master
Reviewed-on: kherel/selfprivacy.org.app#70
2021-10-12 14:22:03 +03:00
kherel d414b22251 Merge pull request 'fix' (#69) from fix-deleting-server into no-hash
Reviewed-on: kherel/selfprivacy.org.app#69
2021-10-12 00:15:19 +03:00
kherel 43732fa866 Merge branch 'no-hash' into fix-deleting-server 2021-10-12 00:14:31 +03:00
Kherel eac9078fad fix 2021-10-11 23:10:04 +02:00
Kherel ba8b19193d update 2021-10-07 18:52:06 +02:00
Kherel 3bc9b23e8f update 2021-10-07 18:52:06 +02:00
kherel 05e4f7e2f8 Merge pull request 'nicolai' (#68) from nicolai into master
Reviewed-on: kherel/selfprivacy.org.app#68
2021-10-07 19:48:59 +03:00
Kherel 4ae77f33c8 fix 2021-10-05 22:43:12 +02:00
tester.nicolai 5c01d6a375 fixed RU app text 2021-10-05 22:38:10 +02:00
kherel e700ea2e4c Merge pull request 'fix' (#66) from fix-sept29 into master
Reviewed-on: kherel/selfprivacy.org.app#66
2021-10-04 11:42:07 +03:00
Kherel 2c4d0ea7d1 fix 2021-09-29 20:28:47 +02:00
kherel 3e7d003f21 service-states (#65)
Co-authored-by: Kherel <kherel@gmail.com>
Reviewed-on: kherel/selfprivacy.org.app#65
Co-authored-by: kherel <kherel@gmail.com>
Co-committed-by: kherel <kherel@gmail.com>
2021-09-29 16:08:19 +03:00
Illia Chub a7e7d0ff05 Merge pull request 'ssh' (#37) from ssh into master
Reviewed-on: kherel/selfprivacy.org.app#37
2021-09-16 17:38:04 +03:00
Kherel 4942f67f37 update 2021-09-15 16:37:22 +02:00
Kherel d0023e5718 update 2021-09-15 15:15:54 +02:00
Illia Chub e064598c73 Merge pull request 'services-switchers' (#32) from services-switchers into master
Reviewed-on: kherel/selfprivacy.org.app#32
2021-09-07 16:30:46 +03:00
Kherel 90d64d8f51 update 2021-09-02 21:32:07 +02:00
Kherel 26607251d9 update 2021-08-29 17:02:51 +02:00
Kherel 84e9259ec2 update 2021-08-29 15:54:28 +02:00
Kherel 94a0e22b15 update 2021-08-29 11:50:24 +02:00
Kherel 1a8a4e7270 update 2021-08-18 12:44:46 +02:00
Kherel 1202e4ad53 change password generator 2021-08-18 11:36:40 +02:00
Illia Chub 8ea1b28d73 Updated application description 2021-08-06 16:27:13 +03:00
Illia Chub 8f8714e07f Added F-Droid changelogs for the latest update 2021-07-29 16:30:13 +03:00
Illia Chub 63d3057125 Merge pull request 'fix build version' (#29) from jobs into master
Reviewed-on: kherel/selfprivacy.org.app#29
2021-07-29 16:22:34 +03:00
Kherel dfc6f67ee3 fix build version 2021-07-29 15:21:18 +02:00
Illia Chub b130960113 Merge pull request 'jobs' (#28) from jobs into master
Reviewed-on: kherel/selfprivacy.org.app#28
2021-07-29 15:10:31 +03:00
Kherel 5dea5234de fix 2021-07-29 14:09:10 +02:00
Kherel 3a5353dbf4 add validation 2021-07-29 11:34:26 +02:00
Kherel 65c6a0b870 fix translations 2021-07-29 08:06:29 +02:00
Kherel 933e8ffb90 fix 2021-07-29 07:29:33 +02:00
Kherel 1c352fd771 update 2021-07-29 07:28:46 +02:00
Kherel Kechil f53ad044c1 finish 2021-07-29 07:24:42 +02:00
Kherel 21611e63c7 commit 2021-06-20 23:08:52 +02:00
Kherel d8393f75ea update 2021-06-20 09:18:25 +02:00
Kherel 9e8fdf2965 update 2021-06-08 20:52:44 +02:00
Kherel d3f494adeb update 2021-05-25 23:53:54 +02:00
kherel e849b551fc Merge pull request 'reverse-dns' (#27) from reverse-dns into master
Reviewed-on: kherel/selfprivacy.org.app#27
2021-05-17 15:52:34 +03:00
Kherel 234515477c fix 2021-05-17 14:46:48 +02:00
Kherel e5758aa2bf fix 2021-05-17 14:40:06 +02:00
Kherel d4f315214b add reverse-dns 2021-05-17 14:38:38 +02:00
109 changed files with 3083 additions and 1826 deletions

16
.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# editorconfig.org
root = true
[*]
indent_size = 2
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.dart]
max_line_length = 150
[*.md]
trim_trailing_whitespace = false

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
ios/build
# Miscellaneous
*.class
*.log
@ -29,7 +30,7 @@
.packages
.pub-cache/
.pub/
/build/
**/build/
# Web related
lib/generated_plugin_registrant.dart

View File

@ -1,12 +1,12 @@
### О проекте
Всё больше организаций хотят владеть нашими данными
А мы сами хотим распоряжаться своими **данными** на своем сервере.
Проект позволяет только Вам в полной мере распоряжаться собственными **данными** на своём сервере.
### Миссия проекта
Цифровая независимость и приватность доступная каждому
Цифровая независимость и приватность доступная каждому.
### Цель
Развивать программу, которая позволит каждому создавать приватные сервисы для себя и своих близких
Развивать программу, которая позволит каждому создавать приватные сервисы для себя и своих близких.

View File

@ -3,5 +3,5 @@
2. Заходим в созданный нами проект. Если такового - нет, значит создаём.
3. Наводим мышкой на боковую панель. Она должна раскрыться, показав нам пункты меню. Нас интересует последний — Security (с иконкой ключика).
4. Далее, в верхней части интерфейса видим примерно такой список: SSH Keys, API Tokens, Certificates, Members. Нам нужен API Tokens. Переходим по нему.
5. В правой части интерфейса, нас будет ожидать кнопка Generate API token. Если же вы используете мобильную версию сайта, в нижнем правом углу вы увидите красный плюсик. Нажимаем на эту кнопку.
6. В поле Description, даём нашему токену название (это может быть любое название, которые вам нравиться. Сути оно не меняет.
5. В правой части интерфейса, нас будет ожидать кнопка Generate API token. Если же Вы используете мобильную версию сайта, в нижнем правом углу Вы увидите красный плюсик. Нажимаем на эту кнопку.
6. В поле Description, даём нашему токену название (это может быть любое название, которые Вам нравиться. Сути оно не меняет.

View File

@ -19,12 +19,15 @@
"connect": "Connect",
"domain": "Domain",
"saving": "Saving..",
"nickname": "nickname",
"nickname": "Nickname",
"loading": "Loading...",
"later": "I will setup it later",
"reset": "Reset",
"details": "Details",
"no_data": "No data"
"no_data": "No data",
"wait": "Wait",
"remove": "Remove",
"apply": "Apply"
},
"more": {
"_comment": "'More' tab",
@ -32,9 +35,22 @@
"about_project": "About us",
"about_app": "About application",
"onboarding": "Onboarding",
"create_ssh_key": "Create ssh key",
"generate_key": "Generate key",
"generate_key_text": "You can generate ssh key",
"console": "Console",
"remove": "Remove",
"enable": "Enable",
"ok": "ok",
"continue": "Continue",
"ssh_key_exist_text": "You have generated ssh key",
"yes_delete": "Yes, delete my SSH key",
"share": "Share",
"copy_buffer": "Copy to buffer",
"copied_ssh": "SSH copied to clipboard",
"delete_ssh_text": "Delete SSH key?",
"about_app_page": {
"text": "Тут любая служебная информация, v.{}"
"text": "Application version v.{}"
},
"settings": {
"title": "Application settings",
@ -74,8 +90,7 @@
"card_title": "Domain",
"status": "Status — Good",
"bottom_sheet": {
"1": "It's your personal internet address that will point to the server and other services of yours.",
"2": "{} — expires on {}"
"1": "It's your personal internet address that will point to the server and other services of yours."
}
},
"backup": {
@ -145,6 +160,13 @@
"bottom_sheet": {
"1": "You can connect and create a new user here:"
}
},
"vpn": {
"title": "VPN Server",
"subtitle": "Private VPN server",
"bottom_sheet": {
"1": "Openconnect VPN Server. Engine for secure and scalable VPN infrastructure"
}
}
},
"users": {
@ -188,8 +210,9 @@
"20": "\n",
"21": "One more restart to apply your security certificates.",
"22": "Create master account",
"23": "Enter a nickname and strong password"
"23": "Enter a nickname and strong password",
"finish": "Everything is initialized",
"checks": "Checks have been completed \n{} ouf of {}"
},
"modals": {
"_comment": "messages in modals",
@ -199,9 +222,29 @@
"4": "Purge all authentication keys?",
"5": "Yes, purge all my tokens",
"6": "Delete the server and volume?",
"7": "Yes"
"7": "Yes",
"8": "Remove task"
},
"timer": {
"sec": "{} sec"
},
"jobs": {
"_comment": "Jobs list",
"title": "Jobs list",
"start": "Start",
"empty": "No jobs",
"createUser": "Create",
"serviceTurnOff": "Turn off",
"serviceTurnOn": "Turn on",
"jobAdded": "Job added",
"runJobs": "Run jobs"
},
"validations": {
"required": "Required",
"invalid_format": "Invalid format",
"root_name": "User name cannot be 'root'",
"key_format": "Invalid key format",
"length": "Length is [] shoud be {}",
"user_alredy_exist": "Already exists"
}
}
}

View File

@ -6,7 +6,7 @@
"providers": "Провайдеры",
"services": "Сервисы",
"users": "Пользователи",
"more": "Еще",
"more": "Ещё",
"next": "Далее",
"got_it": "Понял",
"settings": "Настройки",
@ -22,26 +22,42 @@
"nickname": "Никнейм",
"loading": "Загрузка",
"later": "Настрою потом",
"reset": "Reset",
"reset": "Сбросить",
"details": "Детальная информация",
"no_data": "Нет данных"
"no_data": "Нет данных",
"wait": "Загрузка",
"remove": "Удалить",
"apply": "Подать"
},
"more": {
"_comment": "вкладка еще",
"_comment": "вкладка ещё",
"configuration_wizard": "Мастер Подключения",
"about_project": "О проекте SelfPrivacy",
"about_app": "О приложении",
"onboarding": "Приветствие",
"console": "Консоль",
"create_ssh_key": "Создать ssh ключ",
"generate_key": "Сгенерировать ключ",
"generate_key_text": "Вы сможете сгенерировать ключ",
"remove": "Отключить",
"enable": "Включить",
"ok": "ok",
"continue": "Продолжить",
"ssh_key_exist_text": "У Вас уже есть сгенерированный ssk ключ",
"yes_delete": "Да, удалить",
"share": "Поделиться",
"copy_buffer": "Копировать в буфер",
"copied_ssh": "SSH копировано в буфер",
"delete_ssh_text": "Удалить SSH ключ?",
"about_app_page": {
"text": "Версия приложения: v.{}"
},
"settings": {
"title": "Настройки приложения",
"1": "Темная тема",
"2": "Сменить цветовую тему",
"1": "Тёмная тема",
"2": "Сменить цветовую тему.",
"3": "Сброс настроек",
"4": "Сбросить API ключи а так же root пользвателя",
"4": "Сбросить API ключи а также root пользвателя.",
"5": "Удалить сервер",
"6": "Действие приведет к удалению сервера. После чего он не будет доступен."
}
@ -49,9 +65,9 @@
"onboarding": {
"_comment": "страницы онбординга",
"page1_title": "Цифровая независимость доступна каждому",
"page1_text": "Почта, VPN, Мессенджер, социальная сеть и многое другое на вашем личном сервере, под вашим полным контролем.",
"page2_title": "SelfPrivacy — это не облако, а ваш личный дата-центр",
"page2_text": "У SelfPrivacy работает только с вашими сервис-провадерами: Hetzner, Cloudflare, Backblaze. Если у вас нет учетных записей, мы поможем их создать."
"page1_text": "Почта, VPN, Мессенджер, социальная сеть и многое другое на Вашем личном сервере, под Вашим полным контролем.",
"page2_title": "SelfPrivacy — это не облако, а Ваш личный дата-центр",
"page2_text": "SelfPrivacy работает только с вашими сервис-провайдерами: Hetzner, Cloudflare, Backblaze. Если у Вас нет учётных записей, мы поможем их создать."
},
"providers": {
"_comment": "вкладка провайдеры",
@ -60,7 +76,7 @@
"card_title": "Сервер",
"status": "Статус — в норме",
"bottom_sheet": {
"1": "Это виртульный компьютер на котором работают все ваши сервисы.",
"1": "Это виртуальный компьютер на котором работают все Ваши сервисы.",
"2": "Общая информация",
"3": "Размещение"
},
@ -74,16 +90,15 @@
"card_title": "Домен",
"status": "Статус — в норме",
"bottom_sheet": {
"1": "Это ваш личный адрес в интернете, который будет указывать на сервер и другие ваши сервисы.",
"2": "{} — продлен до {}"
"1": "Это ваш личный адрес в интернете, который будет указывать на сервер и другие ваши сервисы."
}
},
"backup": {
"card_title": "Резервное копирование",
"status": "Статус — в норме",
"bottom_sheet": {
"1": "Выручит в любой ситуации: хакерская атака, удаление сервера и т.п.",
"2": "3Gb — бестплатно до 10Gb, последний вчера в {}"
"1": "Выручит Вас в любой ситуации: хакерская атака, удаление сервера и.т.д.",
"2": "Использовано 3Gb из бестплатых 10Gb. Последнее копирование была сделана вчера в {}."
}
}
},
@ -95,72 +110,79 @@
},
"services": {
"_comment": "Вкладка сервисы",
"title": "Ваши личные приватные и независимые сервисы",
"title": "Ваши личные, приватные и независимые сервисы:",
"mail": {
"title": "Почта",
"subtitle": "Электронная почта для семьи или компании",
"subtitle": "Электронная почта для семьи или компании.",
"bottom_sheet": {
"1": "Для подключения почтового ящика используйте {} и логин пароль, который вы создали. Так же приглашайте",
"2": "новых пользователей"
"1": "Для подключения почтового ящика используйте {} и профиль, который Вы создали. Так же приглашайте",
"2": "новых пользователей."
}
},
"messenger": {
"title": "Мессенджер",
"subtitle": "Telegram и Signal не так приватны, как Delta.Chat использующий ваш личный сервер.",
"subtitle": "Telegram и Signal не так приватны, как Delta.Chat — который использует Ваш личный сервер.",
"bottom_sheet": {
"1": "Для подключения используйте {} и логин пароль, который вы создали"
"1": "Для подключения используйте {} и логин пароль, который Вы создали."
}
},
"password_manager": {
"title": "Менеджер паролей",
"subtitle": "Фундамент безопасности. Создавать, хранить, копировать пароли между устройствами, вбивать их в формы поможет — Bitwarden.",
"subtitle": "Это фундамент Вашей безопасности. Создавать, хранить, копировать пароли между устройствами и вбивать их в формы поможет — Bitwarden.",
"bottom_sheet": {
"1": "Подключиться к серверу и создать пользователя можно по адресу:"
"1": "Подключиться к серверу и создать пользователя можно по адресу:."
}
},
"video": {
"title": "Видеоконференция",
"subtitle": "Zoom и Google meet отличные инструменты, но Jitsi meet не хуже и дает уверенность, что вас никто не подслушивает.",
"subtitle": "Jitsi meet — отличный аналог Zoom и Google meet который по мимо удобства ещё и гарантирует Вам защищённые высококачественные видеоконференции.",
"bottom_sheet": {
"1": "Для использования просто перейдите по ссылке:"
"1": "Для использования просто перейдите по ссылке:."
}
},
"cloud": {
"title": "Файловое облако",
"subtitle": "Не позволяйте облачным сервисам читать ваши данные используйте NextCloud.",
"subtitle": "Не позволяйте облачным сервисам просматривать ваши данные. Используйте NextCloud — надёжный дом для всех Ваших данных.",
"bottom_sheet": {
"1": "Подключиться к серверу и создать пользователя можно по адресу:"
"1": "Подключиться к серверу и создать пользователя можно по адресу:."
}
},
"social_network": {
"title": "Социальная сеть",
"subtitle": "Сложно поверить, но стало возможным создать свою собственную социальную сеть, со своими правилами и аудиторией.",
"bottom_sheet": {
"1": "Подключиться к серверу и создать пользователя можно по адресу:"
"1": "Подключиться к серверу и создать пользователя можно по адресу:."
}
},
"git": {
"title": "Git-сервер",
"subtitle": "Приватная альтернатива Github, которая принадлежит вам, а не Microsoft.",
"bottom_sheet": {
"1": "Подключиться к серверу и создать пользователя можно по адресу:"
"1": "Подключиться к серверу и создать пользователя можно по адресу:."
}
},
"vpn": {
"title": "VPN сервер",
"subtitle": "Закрытый VPN сервер",
"bottom_sheet": {
"1": "Создать подключиться к VPN-серверу. Движок для безопасной и масштабируемой инфраструктуры VPN"
}
}
},
"users": {
"_comment": "'Users' tab",
"add_new_user": "Добавьте первого пользователя",
"add_new_user": "Добавьте первого пользователя.",
"new_user": "Новый пользователь",
"not_ready": "Подключите сервер, домен и DNS в разделу Провайдеры, чтобы добавить первого пользователя",
"nobody_here": "'Здесь пока никого'",
"nobody_here": "Здесь будут отображаться пользователи.",
"login": "Логин",
"onboarding": "Приветствие",
"console": "Консоль разработчика",
"new_user_info_note": "Новый пользователь автоматически получит доступ ко всем сервисам. Ещё какое-то описание.",
"delete_confirm_question": "удалить учетную запись?",
"new_user_info_note": "Новый пользователь автоматически получит доступ ко всем сервисам.",
"delete_confirm_question": "Вы действительно хотите удалить учетную запись?",
"reset_password": "Сбросить пароль",
"account": "Учетная запись",
"send_regisration_data": "Отправить реквизиты для входа"
"send_regisration_data": "Поделиться реквизитами"
},
"initializing": {
"_comment": "initializing page",
@ -173,34 +195,56 @@
"6": "Подключите облачное хранилище Backblaze",
"7": "На данный момент подлюченных доменов нет",
"8": "Загружаем список доменов",
"9": "Найдено больше одного домена, для вашей безопастности, просим вам удалить не нужные домены",
"9": "Найдено больше одного домена, для вашей безопастности, просим Вам удалить не нужные домены",
"10": "Сохранить домен",
"final": "Последний шаг",
"11": "Создать сервер",
"what": "Что это значит?",
"13": "Сервер презагружен, ждем последнюю проверку",
"14": "Cервер запушен, сейчас он будет проверен и перезагружен",
"15": "Cервер создан, идет проверка ДНС адресов и запуск сервера",
"13": "Сервер презагружен, ждем последнюю проверку.",
"14": "Cервер запущен, сейчас он будет проверен и перезагружен.",
"15": "Cервер создан, идет проверка ДНС адресов и запуск сервера.",
"16": "До следующей проверки: ",
"17": "Проверка",
"18": "Как получить Hetzner API Token'",
"18": "Как получить Hetzner API Token:'",
"19": "1 Переходим по ссылке ",
"20": "\n2 Заходим в созданный нами проект. Если такового - нет, значит создаём.\n3 Наводим мышкой на боковую панель. Она должна раскрыться, показав нам пункты меню. Нас интересует последний — Security (с иконкой ключика).\n4 Далее, в верхней части интерфейса видим примерно такой список: SSH Keys, API Tokens, Certificates, Members. Нам нужен API Tokens. Переходим по нему.\n5 В правой части интерфейса, нас будет ожидать кнопка Generate API token. Если же вы используете мобильную версию сайта, в нижнем правом углу вы увидите красный плюсик. Нажимаем на эту кнопку.\n6 В поле Description, даём нашему токену название (это может быть любое название, которые вам нравиться. Сути оно не меняет.",
"20": "\n2 Заходим в созданный нами проект. Если такового нет - значит создаём.\n3 Наводим мышкой на боковую панель. Она должна раскрыться, показав нам пункты меню. Нас интересует последний — Security (с иконкой ключика).\n4 Далее, в верхней части интерфейса видим примерно такой список: SSH Keys, API Tokens, Certificates, Members. Нам нужен API Tokens. Переходим по нему.\n5 В правой части интерфейса, нас будет ожидать кнопка Generate API token. Если же Вы используете мобильную версию сайта - в нижнем правом углу Вы увидите красный плюсик. Нажимаем на эту кнопку.\n6 В поле Description даём нашему токену название (это может быть любое название, которое Вам нравиться, сути оно не меняет.",
"21": "Сейчас будет дополнительная перезагрузка для активации сертификатов безопастности",
"22": "Создайте главную учетную запись",
"23": "Введите никнейм и сложный пароль"
"23": "Введите никнейм и сложный пароль",
"finish": "Всё инициализировано.",
"checks": "Проверок выполнено: \n{} / {}"
},
"modals": {
"_comment": "messages in modals",
"1": "Сервер с таким именем уже существует",
"2": "Уничтожить сервер и создать новый?",
"3": "Вы уверенны",
"3": "Подтвердите",
"4": "Сбросить все ключи?",
"5": "Да, сбросить",
"6": "Удалить сервер и диск?",
"7": "Да, удалить"
"7": "Да, удалить",
"8": "Удалить задачу"
},
"timer": {
"sec": "{} сек"
},
"jobs": {
"_comment": "Jobs list",
"title": "Задачи",
"start": "Начать выполенение",
"empty": "Пусто.",
"createUser": "Создать запись",
"serviceTurnOff": "Остановить",
"serviceTurnOn": "Запустить",
"jobAdded": "Задача добавленна",
"runJobs": "Запустите задачи"
},
"validations": {
"required": "Обязательное поле.",
"invalid_format": "Неверный формат.",
"root_name": "Имя пользователя не может быть'root'.",
"key_format": "Неверный формат.",
"length": "Длина строки [] должна быть {}.",
"user_alredy_exist": "Имя уже используется."
}
}
}

View File

@ -0,0 +1,7 @@
- Added users creation possibility
- Fixed reverse DNS issue during server creation
- Updated NixOS to 21.05
- Defined application users as system ones
- Minor bugfixes
- Translation fixes
- en_GB is now a separate locale

View File

@ -12,5 +12,4 @@ Application will do the following things for you:
* Gitea - your own Git server
* OpenConnect - Personal VPN server
!!! Project is currently in alpha state. Feel free to try it. It would be much appreciated if you would provide us with some feedback.
!!! Only Russian localization is currently available. English is coming soon
!!! Project is currently in open beta state. Feel free to try it. It would be much appreciated if you would provide us with some feedback.

View File

@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>8.0</string>
<string>9.0</string>
</dict>
</plist>

View File

@ -2,10 +2,14 @@ PODS:
- Flutter (1.0.0)
- flutter_secure_storage (3.3.1):
- Flutter
- local_auth (0.0.1):
- Flutter
- package_info (0.0.1):
- Flutter
- path_provider (0.0.1):
- Flutter
- share_plus (0.0.1):
- Flutter
- shared_preferences (0.0.1):
- Flutter
- url_launcher (0.0.1):
@ -16,8 +20,10 @@ PODS:
DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- local_auth (from `.symlinks/plugins/local_auth/ios`)
- package_info (from `.symlinks/plugins/package_info/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
- url_launcher (from `.symlinks/plugins/url_launcher/ios`)
- wakelock (from `.symlinks/plugins/wakelock/ios`)
@ -27,10 +33,14 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
local_auth:
:path: ".symlinks/plugins/local_auth/ios"
package_info:
:path: ".symlinks/plugins/package_info/ios"
path_provider:
:path: ".symlinks/plugins/path_provider/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences:
:path: ".symlinks/plugins/shared_preferences/ios"
url_launcher:
@ -39,10 +49,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock/ios"
SPEC CHECKSUMS:
Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
local_auth: 25938960984c3a7f6e3253e3f8d962fdd16852bd
package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62
path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d
url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f

View File

@ -345,7 +345,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@ -360,7 +360,8 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_TEAM = UVNTKR53DD;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -379,6 +380,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
@ -431,7 +433,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@ -481,7 +483,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@ -498,7 +500,8 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_TEAM = UVNTKR53DD;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -518,6 +521,7 @@
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@ -528,7 +532,8 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_TEAM = UVNTKR53DD;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -547,6 +552,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 608 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 906 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 525 B

After

Width:  |  Height:  |  Size: 1007 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 907 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 608 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -2,11 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleLocalizations</key>
<array>
<string>ru</string>
<string>en</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
@ -15,6 +10,11 @@
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>ru</string>
<string>en</string>
</array>
<key>CFBundleName</key>
<string>selfprivacy</string>
<key>CFBundlePackageType</key>
@ -24,7 +24,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
@ -34,8 +34,6 @@
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
class BlocAndProviderConfig extends StatelessWidget {
@ -12,11 +14,10 @@ class BlocAndProviderConfig extends StatelessWidget {
@override
Widget build(BuildContext context) {
// var platformBrightness =
// SchedulerBinding.instance.window.platformBrightness;
// var isDark = platformBrightness == Brightness.dark;
var isDark = false;
var usersCubit = UsersCubit();
var appConfigCubit = AppConfigCubit()..load();
var servicesCubit = ServicesCubit(appConfigCubit);
return MultiProvider(
providers: [
BlocProvider(
@ -25,12 +26,14 @@ class BlocAndProviderConfig extends StatelessWidget {
isOnbordingShowing: true,
)..load(),
),
BlocProvider(
lazy: false,
create: (_) => AppConfigCubit()..load(),
),
BlocProvider(lazy: false, create: (_) => appConfigCubit),
BlocProvider(create: (_) => ProvidersCubit()),
BlocProvider(create: (_) => UsersCubit()),
BlocProvider(create: (_) => usersCubit..load(), lazy: false),
BlocProvider(create: (_) => servicesCubit..load(), lazy: false),
BlocProvider(
create: (_) =>
JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit),
),
],
child: child,
);

View File

@ -1,7 +0,0 @@
import 'package:flutter/material.dart';
final shadow8 = BoxShadow(
offset: Offset(0, 4),
blurRadius: 8,
color: Colors.black.withOpacity(.08),
);

View File

@ -76,6 +76,6 @@ var darkTheme = ligtTheme.copyWith(
),
);
final brandPagePadding1 = EdgeInsets.symmetric(horizontal: 15, vertical: 30);
final paddingH15V30 = EdgeInsets.symmetric(horizontal: 15, vertical: 30);
final brandPagePadding2 = EdgeInsets.symmetric(horizontal: 15);
final paddingH15V0 = EdgeInsets.symmetric(horizontal: 15);

View File

@ -2,6 +2,7 @@ import 'package:get_it/get_it.dart';
import 'package:selfprivacy/logic/get_it/api_config.dart';
import 'package:selfprivacy/logic/get_it/console.dart';
import 'package:selfprivacy/logic/get_it/navigation.dart';
import 'package:selfprivacy/logic/get_it/ssh.dart';
import 'package:selfprivacy/logic/get_it/timer.dart';
export 'package:selfprivacy/logic/get_it/api_config.dart';
@ -9,7 +10,6 @@ export 'package:selfprivacy/logic/get_it/console.dart';
export 'package:selfprivacy/logic/get_it/navigation.dart';
export 'package:selfprivacy/logic/get_it/timer.dart';
final getIt = GetIt.instance;
Future<void> getItSetup() async {
@ -17,6 +17,7 @@ Future<void> getItSetup() async {
getIt.registerSingleton<ConsoleModel>(ConsoleModel());
getIt.registerSingleton<TimerModel>(TimerModel());
getIt.registerSingleton<SSHModel>(SSHModel()..init());
getIt.registerSingleton<ApiConfigModel>(ApiConfigModel()..init());
await getIt.allReady();

View File

@ -19,21 +19,25 @@ class HiveConfig {
Hive.registerAdapter(HetznerDataBaseAdapter());
await Hive.openBox(BNames.appSettings);
var cipher = HiveAesCipher(await getEncriptedKey());
await Hive.openBox<User>(BNames.users);
await Hive.openBox(BNames.servicesState);
var cipher = HiveAesCipher(await getEncriptedKey(BNames.key));
await Hive.openBox(BNames.appConfig, encryptionCipher: cipher);
var sshCipher = HiveAesCipher(await getEncriptedKey(BNames.sshEnckey));
await Hive.openBox(BNames.sshConfig, encryptionCipher: sshCipher);
}
static Future<Uint8List> getEncriptedKey() async {
final FlutterSecureStorage secureStorage = FlutterSecureStorage();
var containsEncryptionKey =
await secureStorage.containsKey(key: BNames.key);
if (!containsEncryptionKey) {
static Future<Uint8List> getEncriptedKey(String encKey) async {
final secureStorage = FlutterSecureStorage();
var hasEncryptionKey = await secureStorage.containsKey(key: encKey);
if (!hasEncryptionKey) {
var key = Hive.generateSecureKey();
await secureStorage.write(key: BNames.key, value: base64UrlEncode(key));
await secureStorage.write(key: encKey, value: base64UrlEncode(key));
}
String? string = await secureStorage.read(key: BNames.key);
String? string = await secureStorage.read(key: encKey);
return base64Url.decode(string!);
}
}
@ -42,10 +46,13 @@ class BNames {
static String appConfig = 'appConfig';
static String isDarkModeOn = 'isDarkModeOn';
static String isOnbordingShowing = 'isOnbordingShowing';
static String users = 'users';
static String appSettings = 'appSettings';
static String servicesState = 'servicesState';
static String key = 'key';
static String sshEnckey = 'sshEngkey';
static String cloudFlareDomain = 'cloudFlareDomain';
static String hetznerKey = 'hetznerKey';
@ -58,4 +65,7 @@ class BNames {
static String isLoading = 'isLoading';
static String isServerResetedFirstTime = 'isServerResetedFirstTime';
static String isServerResetedSecondTime = 'isServerResetedSecondTime';
static String sshConfig = 'sshConfig';
static String sshPrivateKey = "sshPrivateKey";
static String sshPublicKey = "sshPublicKey";
}

View File

@ -38,6 +38,13 @@ final headline4Style = defaultTextStyle.copyWith(
color: BrandColors.headlineColor,
);
final headline4UnderlinedStyle = defaultTextStyle.copyWith(
fontSize: 18,
fontWeight: NamedFontWeight.medium,
color: BrandColors.headlineColor,
decoration: TextDecoration.underline,
);
final headline5Style = defaultTextStyle.copyWith(
fontSize: 15,
fontWeight: NamedFontWeight.medium,

View File

@ -15,13 +15,23 @@ abstract class ApiMap {
if (hasLoger) {
dio.interceptors.add(PrettyDioLogger());
}
dio..interceptors.add(ConsoleInterceptor());
dio.interceptors.add(ConsoleInterceptor());
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(HttpClient client) {
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
return client;
};
dio.interceptors.add(InterceptorsWrapper(onError: (DioError e, handler) {
print(e.requestOptions.path);
print(e.requestOptions.data);
print(e.message);
print(e.response);
return handler.next(e);
}));
return dio;
}

View File

@ -7,7 +7,7 @@ import 'package:selfprivacy/logic/api_maps/api_map.dart';
import 'package:selfprivacy/logic/models/hetzner_server_info.dart';
import 'package:selfprivacy/logic/models/server_details.dart';
import 'package:selfprivacy/logic/models/user.dart';
import 'package:selfprivacy/utils/password_generator2.dart';
import 'package:selfprivacy/utils/password_generator.dart';
class HetznerApi extends ApiMap {
bool hasLoger;
@ -68,24 +68,13 @@ class HetznerApi extends ApiMap {
return server == null;
}
Future<HetznerServerDetails> createServer({
required String cloudFlareKey,
required User rootUser,
required String domainName,
}) async {
var dbPassword = getRandomString(40);
const chars =
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
var dbStorageName = getRandomString(6, chars);
Future<HetznerDataBase> createVolume() async {
var client = await getClient();
Response dbCreateResponse = await client.post(
'/volumes',
data: {
"size": 10,
"name": dbStorageName,
"name": StringGenerators.dbStorageName(),
"labels": {"labelkey": "value"},
"location": "fsn1",
"automount": false,
@ -93,10 +82,41 @@ class HetznerApi extends ApiMap {
},
);
var dbId = dbCreateResponse.data['volume']['id'];
return HetznerDataBase(
id: dbId,
name: dbCreateResponse.data['volume']['name'],
);
}
Future<HetznerServerDetails> createServer({
required String cloudFlareKey,
required User rootUser,
required String domainName,
required HetznerDataBase dataBase,
}) async {
var client = await getClient();
// Response dbCreateResponse = await client.post(
// '/volumes',
// data: {
// "size": 10,
// "name": StringGenerators.dbStorageName(),
// "labels": {"labelkey": "value"},
// "location": "fsn1",
// "automount": false,
// "format": "ext4"
// },
// );
var dbPassword = StringGenerators.dbPassword();
// var dbId = dbCreateResponse.data['volume']['id'];
var dbId = dataBase.id;
/// add ssh key when you need it: e.g. "ssh_keys":["kherel"]
/// check the branch name, it could be "development" or "master".
var data = jsonDecode(
'''{"name":"$domainName","server_type":"cx11","start_after_create":false,"image":"ubuntu-20.04", "volumes":[$dbId], "networks":[],"user_data":"#cloud-config\\nruncmd:\\n- curl https://git.selfprivacy.org/ilchub/selfprivacy-nixos-infect/raw/branch/master/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-20.09 DOMAIN=$domainName LUSER=${rootUser.login} PASSWORD=${rootUser.password} HASHED_PASSWORD=${rootUser.hashPassword.hash} SALT=${rootUser.hashPassword.salt} CF_TOKEN=$cloudFlareKey DB_PASSWORD=$dbPassword bash 2>&1 | tee /tmp/infect.log","labels":{},"automount":true, "location": "fsn1"}''',
);
'''{"name":"$domainName","server_type":"cx11","start_after_create":false,"image":"ubuntu-20.04", "volumes":[$dbId], "networks":[], "user_data":"#cloud-config\\nruncmd:\\n- curl https://git.selfprivacy.org/ilchub/selfprivacy-nixos-infect/raw/branch/development/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-21.05 DOMAIN=$domainName LUSER=${rootUser.login} PASSWORD=${rootUser.password} CF_TOKEN=$cloudFlareKey DB_PASSWORD=$dbPassword bash 2>&1 | tee /tmp/infect.log","labels":{},"automount":true, "location": "fsn1"}''');
Response serverCreateResponse = await client.post(
'/servers',
@ -108,10 +128,7 @@ class HetznerApi extends ApiMap {
id: serverCreateResponse.data['server']['id'],
ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'],
createTime: DateTime.now(),
dataBase: HetznerDataBase(
id: dbId,
name: dbCreateResponse.data['volume']['name'],
),
dataBase: dataBase,
);
}
@ -122,28 +139,22 @@ class HetznerApi extends ApiMap {
Response serversReponse = await client.get('/servers');
List servers = serversReponse.data['servers'];
var server = servers.firstWhere((el) => el['name'] == domainName);
await client.delete('/servers/${server['id']}');
Response volumesReponse = await client.get('/volumes');
List volumes = volumesReponse.data['volumes'];
Map server = servers.firstWhere((el) => el['name'] == domainName);
List volumes = server['volumes'];
var laterFutures = <Future>[];
for (var volume in volumes) {
if (volume['server'] == null) {
await client.delete('/volumes/${volume['id']}');
} else {
laterFutures.add(Future.delayed(Duration(seconds: 60)).then(
(_) => client.delete('/volumes/${volume['id']}'),
));
}
}
if (laterFutures.isEmpty) {
close(client);
} else {
Future.wait(laterFutures).then((value) => close(client));
for (var volumeId in volumes) {
await client.post('/volumes/$volumeId/actions/detach');
}
await Future.delayed(Duration(seconds: 10));
for (var volumeId in volumes) {
laterFutures.add(client.delete('/volumes/$volumeId'));
}
laterFutures.add(client.delete('/servers/${server['id']}'));
await Future.wait(laterFutures);
close(client);
}
Future<HetznerServerDetails> reset() async {
@ -192,4 +203,20 @@ class HetznerApi extends ApiMap {
return HetznerServerInfo.fromJson(response.data!['server']);
}
Future<void> createReverseDns({
required String ip4,
required String domainName,
}) async {
var hetznerServer = getIt<ApiConfigModel>().hetznerServer;
var client = await getClient();
await client.post(
'/servers/${hetznerServer!.id}/actions/change_dns_ptr',
data: {
"ip": ip4,
"dns_ptr": domainName,
},
);
close(client);
}
}

View File

@ -3,6 +3,8 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/models/user.dart';
import 'api_map.dart';
@ -31,7 +33,7 @@ class ServerApi extends ApiMap {
var client = await getClient();
try {
response = await client.get('/serviceStatus');
response = await client.get('/services/status');
res = response.statusCode == HttpStatus.ok;
} catch (e) {
res = false;
@ -40,6 +42,106 @@ class ServerApi extends ApiMap {
return res;
}
Future<bool> createUser(User user) async {
bool res;
Response response;
var client = await getClient();
try {
response = await client.post(
'/users/create',
options: Options(
headers: {
"X-User": user.login,
"X-Password": user.password,
"X-Domain": getIt<ApiConfigModel>().cloudFlareDomain!.domainName
},
),
);
res = response.statusCode == HttpStatus.ok;
} catch (e) {
print(e);
res = false;
}
close(client);
return res;
}
String get rootAddress =>
throw UnimplementedError('not used in with implementation');
Future<bool> apply() async {
bool res;
Response response;
var client = await getClient();
try {
response = await client.get(
'/system/configuration/apply',
);
res = response.statusCode == HttpStatus.ok;
} catch (e) {
print(e);
res = false;
}
close(client);
return res;
}
Future<void> switchService(ServiceTypes type, bool needToTurnOn) async {
var client = await getClient();
client.post('/services/${type.url}/${needToTurnOn ? 'enable' : 'disable'}');
client.close();
}
Future<void> sendSsh(String ssh) async {
var client = await getClient();
client.post(
'/services/ssh/enable',
data: {"public_key": ssh},
);
client.close();
}
Future<Map<ServiceTypes, bool>> servicesPowerCheck() async {
var client = await getClient();
Response response = await client.get('/services/status');
close(client);
return {
ServiceTypes.passwordManager: response.data['bitwarden'] == 0,
ServiceTypes.git: response.data['gitea'] == 0,
ServiceTypes.cloud: response.data['nextcloud'] == 0,
ServiceTypes.vpn: response.data['ocserv'] == 0,
ServiceTypes.socialNetwork: response.data['pleroma'] == 0,
};
}
}
extension UrlServerExt on ServiceTypes {
String get url {
switch (this) {
// case ServiceTypes.mail:
// return ''; // cannot be swithch off
// case ServiceTypes.messenger:
// return ''; // external service
// case ServiceTypes.video:
// return ''; // jeetsu meet not working
case ServiceTypes.passwordManager:
return 'bitwarden';
case ServiceTypes.cloud:
return 'nextcloud';
case ServiceTypes.socialNetwork:
return 'pleroma';
case ServiceTypes.git:
return 'gitea';
case ServiceTypes.vpn:
return 'ocserv';
default:
throw Exception('wrong state');
}
}
}

View File

@ -1,3 +1,8 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:ionicons/ionicons.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
enum InitializingSteps {
setHeznerKey,
setCloudFlareKey,
@ -10,3 +15,80 @@ enum InitializingSteps {
}
enum Period { hour, day, month }
enum ServiceTypes {
mail,
messenger,
passwordManager,
video,
cloud,
socialNetwork,
git,
vpn,
}
extension ServiceTypesExt on ServiceTypes {
String get title {
switch (this) {
case ServiceTypes.mail:
return 'services.mail.title'.tr();
case ServiceTypes.messenger:
return 'services.messenger.title'.tr();
case ServiceTypes.passwordManager:
return 'services.password_manager.title'.tr();
case ServiceTypes.video:
return 'services.video.title'.tr();
case ServiceTypes.cloud:
return 'services.cloud.title'.tr();
case ServiceTypes.socialNetwork:
return 'services.social_network.title'.tr();
case ServiceTypes.git:
return 'services.git.title'.tr();
case ServiceTypes.vpn:
return 'services.vpn.title'.tr();
}
}
String get subtitle {
switch (this) {
case ServiceTypes.mail:
return 'services.mail.subtitle'.tr();
case ServiceTypes.messenger:
return 'services.messenger.subtitle'.tr();
case ServiceTypes.passwordManager:
return 'services.password_manager.subtitle'.tr();
case ServiceTypes.video:
return 'services.video.subtitle'.tr();
case ServiceTypes.cloud:
return 'services.cloud.subtitle'.tr();
case ServiceTypes.socialNetwork:
return 'services.social_network.subtitle'.tr();
case ServiceTypes.git:
return 'services.git.subtitle'.tr();
case ServiceTypes.vpn:
return 'services.vpn.subtitle'.tr();
}
}
IconData get icon {
switch (this) {
case ServiceTypes.mail:
return BrandIcons.envelope;
case ServiceTypes.messenger:
return BrandIcons.messanger;
case ServiceTypes.passwordManager:
return BrandIcons.key;
case ServiceTypes.video:
return BrandIcons.webcam;
case ServiceTypes.cloud:
return BrandIcons.upload;
case ServiceTypes.socialNetwork:
return BrandIcons.social;
case ServiceTypes.git:
return BrandIcons.git;
case ServiceTypes.vpn:
return Ionicons.shield_checkmark_outline;
}
}
String get txt => this.toString().split('.')[1];
}

View File

@ -2,6 +2,8 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/get_it/ssh.dart';
import 'package:selfprivacy/logic/models/backblaze_credential.dart';
import 'package:selfprivacy/logic/models/cloudflare_domain.dart';
@ -43,31 +45,35 @@ part 'app_config_state.dart';
/// c. if server is okay set that fully checked
class AppConfigCubit extends Cubit<AppConfigState> {
AppConfigCubit() : super(InitialAppConfigState());
AppConfigCubit() : super(AppConfigEmpty());
final repository = AppConfigRepository();
Future<void> load() async {
var state = await repository.load();
if (state.progress < 6 || state.isFullyInitilized) {
if (state is AppConfigFinished) {
emit(state);
} else if (state.progress == 6) {
startServerIfDnsIsOkay(state: state, isImmediate: true);
} else if (state.progress == 7) {
resetServerIfServerIsOkay(state: state, isImmediate: true);
} else if (state.progress == 8) {
oneMoreReset(state: state, isImmediate: true);
} else if (state.progress == 9) {
finishCheckIfServerIsOkay(state: state, isImmediate: true);
} else if (state is AppConfigNotFinished) {
if (state.progress == 6) {
startServerIfDnsIsOkay(state: state, isImmediate: true);
} else if (state.progress == 7) {
resetServerIfServerIsOkay(state: state, isImmediate: true);
} else if (state.progress == 8) {
oneMoreReset(state: state, isImmediate: true);
} else if (state.progress == 9) {
finishCheckIfServerIsOkay(state: state, isImmediate: true);
}
} else {
throw 'wrong state';
}
}
void startServerIfDnsIsOkay({
AppConfigState? state,
AppConfigNotFinished? state,
bool isImmediate = false,
}) async {
state = state ?? this.state;
state = state ?? this.state as AppConfigNotFinished;
final work = () async {
emit(TimerState(dataState: state!, isLoading: true));
@ -112,10 +118,10 @@ class AppConfigCubit extends Cubit<AppConfigState> {
}
void oneMoreReset({
AppConfigState? state,
AppConfigNotFinished? state,
bool isImmediate = false,
}) async {
var dataState = state ?? this.state;
var dataState = state ?? this.state as AppConfigNotFinished;
var work = () async {
emit(TimerState(dataState: dataState, isLoading: true));
@ -165,10 +171,10 @@ class AppConfigCubit extends Cubit<AppConfigState> {
}
void resetServerIfServerIsOkay({
AppConfigState? state,
AppConfigNotFinished? state,
bool isImmediate = false,
}) async {
var dataState = state ?? this.state;
var dataState = state ?? this.state as AppConfigNotFinished;
var work = () async {
emit(TimerState(dataState: dataState, isLoading: true));
@ -220,10 +226,10 @@ class AppConfigCubit extends Cubit<AppConfigState> {
Timer? timer;
void finishCheckIfServerIsOkay({
AppConfigState? state,
AppConfigNotFinished? state,
bool isImmediate = false,
}) async {
state = state ?? this.state;
state = state ?? this.state as AppConfigNotFinished;
var work = () async {
emit(TimerState(dataState: state!, isLoading: true));
@ -233,10 +239,7 @@ class AppConfigCubit extends Cubit<AppConfigState> {
if (isServerWorking) {
await repository.saveHasFinalChecked(true);
emit(state.copyWith(
hasFinalChecked: true,
isLoading: false,
));
emit(state.finish());
} else {
finishCheckIfServerIsOkay();
}
@ -259,40 +262,42 @@ class AppConfigCubit extends Cubit<AppConfigState> {
void clearAppConfig() {
closeTimer();
repository.clearAppConfig();
emit(InitialAppConfigState());
emit(AppConfigEmpty());
}
Future<void> serverDelete() async {
closeTimer();
if (state.hetznerServer != null) {
await repository.deleteServer(state.cloudFlareDomain!);
await getIt<SSHModel>().clear();
}
await repository.deleteRecords();
emit(AppConfigState(
emit(AppConfigNotFinished(
hetznerKey: state.hetznerKey,
cloudFlareDomain: state.cloudFlareDomain,
cloudFlareKey: state.cloudFlareKey,
backblazeCredential: state.backblazeCredential,
cloudFlareDomain: state.cloudFlareDomain,
rootUser: state.rootUser,
hetznerServer: null,
isServerStarted: false,
isServerResetedFirstTime: false,
isServerResetedSecondTime: false,
hasFinalChecked: false,
isLoading: false,
error: null,
));
}
void setHetznerKey(String hetznerKey) async {
await repository.saveHetznerKey(hetznerKey);
emit(state.copyWith(hetznerKey: hetznerKey));
emit((state as AppConfigNotFinished).copyWith(hetznerKey: hetznerKey));
}
void setCloudflareKey(String cloudFlareKey) async {
await repository.saveCloudFlareKey(cloudFlareKey);
emit(state.copyWith(cloudFlareKey: cloudFlareKey));
emit(
(state as AppConfigNotFinished).copyWith(cloudFlareKey: cloudFlareKey));
}
void setBackblazeKey(String keyId, String applicationKey) async {
@ -301,38 +306,41 @@ class AppConfigCubit extends Cubit<AppConfigState> {
applicationKey: applicationKey,
);
await repository.saveBackblazeKey(backblazeCredential);
emit(state.copyWith(backblazeCredential: backblazeCredential));
emit((state as AppConfigNotFinished)
.copyWith(backblazeCredential: backblazeCredential));
}
void setDomain(CloudFlareDomain cloudFlareDomain) async {
await repository.saveDomain(cloudFlareDomain);
emit(state.copyWith(cloudFlareDomain: cloudFlareDomain));
emit((state as AppConfigNotFinished)
.copyWith(cloudFlareDomain: cloudFlareDomain));
}
void setRootUser(User rootUser) async {
await repository.saveRootUser(rootUser);
emit(state.copyWith(rootUser: rootUser));
emit((state as AppConfigNotFinished).copyWith(rootUser: rootUser));
}
void createServerAndSetDnsRecords() async {
AppConfigState _stateCopy = state;
var onSuccess = (serverDetails) async {
AppConfigNotFinished _stateCopy = state as AppConfigNotFinished;
var onSuccess = (HetznerServerDetails serverDetails) async {
await repository.createDnsRecords(
serverDetails.ip4,
state.cloudFlareDomain!,
);
emit(state.copyWith(
emit((state as AppConfigNotFinished).copyWith(
isLoading: false,
hetznerServer: serverDetails,
));
startServerIfDnsIsOkay();
};
var onCancel = () => emit(state.copyWith(isLoading: false));
var onCancel =
() => emit((state as AppConfigNotFinished).copyWith(isLoading: false));
try {
emit(state.copyWith(isLoading: true));
emit((state as AppConfigNotFinished).copyWith(isLoading: true));
await repository.createServer(
state.rootUser!,
state.cloudFlareDomain!.domainName,

View File

@ -22,22 +22,37 @@ class AppConfigRepository {
Box box = Hive.box(BNames.appConfig);
Future<AppConfigState> load() async {
var res = AppConfigState(
hetznerKey: getIt<ApiConfigModel>().hetznerKey,
cloudFlareKey: getIt<ApiConfigModel>().cloudFlareKey,
cloudFlareDomain: getIt<ApiConfigModel>().cloudFlareDomain,
backblazeCredential: getIt<ApiConfigModel>().backblazeCredential,
hetznerServer: getIt<ApiConfigModel>().hetznerServer,
rootUser: box.get(BNames.rootUser),
isServerStarted: box.get(BNames.isServerStarted, defaultValue: false),
isServerResetedFirstTime:
box.get(BNames.isServerResetedFirstTime, defaultValue: false),
isServerResetedSecondTime:
box.get(BNames.isServerResetedSecondTime, defaultValue: false),
hasFinalChecked: box.get(BNames.hasFinalChecked, defaultValue: false),
error: null,
isLoading: box.get(BNames.isLoading, defaultValue: false),
);
late AppConfigState res;
if (box.get(BNames.hasFinalChecked, defaultValue: false)) {
res = AppConfigFinished(
hetznerKey: getIt<ApiConfigModel>().hetznerKey!,
cloudFlareKey: getIt<ApiConfigModel>().cloudFlareKey!,
cloudFlareDomain: getIt<ApiConfigModel>().cloudFlareDomain!,
backblazeCredential: getIt<ApiConfigModel>().backblazeCredential!,
hetznerServer: getIt<ApiConfigModel>().hetznerServer!,
rootUser: box.get(BNames.rootUser),
isServerStarted: box.get(BNames.isServerStarted, defaultValue: false),
isServerResetedFirstTime:
box.get(BNames.isServerResetedFirstTime, defaultValue: false),
isServerResetedSecondTime:
box.get(BNames.isServerResetedSecondTime, defaultValue: false),
);
} else {
res = AppConfigNotFinished(
hetznerKey: getIt<ApiConfigModel>().hetznerKey,
cloudFlareKey: getIt<ApiConfigModel>().cloudFlareKey,
cloudFlareDomain: getIt<ApiConfigModel>().cloudFlareDomain,
backblazeCredential: getIt<ApiConfigModel>().backblazeCredential,
hetznerServer: getIt<ApiConfigModel>().hetznerServer,
rootUser: box.get(BNames.rootUser),
isServerStarted: box.get(BNames.isServerStarted, defaultValue: false),
isServerResetedFirstTime:
box.get(BNames.isServerResetedFirstTime, defaultValue: false),
isServerResetedSecondTime:
box.get(BNames.isServerResetedSecondTime, defaultValue: false),
isLoading: box.get(BNames.isLoading, defaultValue: false),
);
}
return res;
}
@ -101,12 +116,16 @@ class AppConfigRepository {
onSuccess,
}) async {
var hetznerApi = HetznerApi();
late HetznerDataBase dataBase;
try {
dataBase = await hetznerApi.createVolume();
var serverDetails = await hetznerApi.createServer(
cloudFlareKey: cloudFlareKey,
rootUser: rootUser,
domainName: domainName,
dataBase: dataBase,
);
saveServerDetails(serverDetails);
onSuccess(serverDetails);
@ -129,6 +148,7 @@ class AppConfigRepository {
cloudFlareKey: cloudFlareKey,
rootUser: rootUser,
domainName: domainName,
dataBase: dataBase,
);
await saveServerDetails(serverDetails);
@ -149,7 +169,7 @@ class AppConfigRepository {
}
Future<void> createDnsRecords(
String? ip4,
String ip4,
CloudFlareDomain cloudFlareDomain,
) async {
var cloudflareApi = CloudflareApi();
@ -163,6 +183,11 @@ class AppConfigRepository {
ip4: ip4,
cloudFlareDomain: cloudFlareDomain,
);
await HetznerApi().createReverseDns(
ip4: ip4,
domainName: cloudFlareDomain.domainName,
);
}
Future<bool> isHttpServerWorking() async {
@ -225,10 +250,11 @@ class AppConfigRepository {
var hetznerApi = HetznerApi();
var cloudFlare = CloudflareApi();
hetznerApi.deleteSelfprivacyServerAndAllVolumes(
await hetznerApi.deleteSelfprivacyServerAndAllVolumes(
domainName: cloudFlareDomain.domainName,
);
cloudFlare.removeSimilarRecords(cloudFlareDomain: cloudFlareDomain);
await cloudFlare.removeSimilarRecords(cloudFlareDomain: cloudFlareDomain);
}
Future<void> deleteRecords() async {

View File

@ -1,6 +1,6 @@
part of 'app_config_cubit.dart';
class AppConfigState extends Equatable {
abstract class AppConfigState extends Equatable {
const AppConfigState({
required this.hetznerKey,
required this.cloudFlareKey,
@ -11,9 +11,6 @@ class AppConfigState extends Equatable {
required this.isServerStarted,
required this.isServerResetedFirstTime,
required this.isServerResetedSecondTime,
required this.hasFinalChecked,
required this.isLoading,
required this.error,
});
@override
@ -26,9 +23,6 @@ class AppConfigState extends Equatable {
hetznerServer,
isServerStarted,
isServerResetedFirstTime,
hasFinalChecked,
isLoading,
error,
];
final String? hetznerKey;
@ -41,42 +35,6 @@ class AppConfigState extends Equatable {
final bool isServerResetedFirstTime;
final bool isServerResetedSecondTime;
final bool hasFinalChecked;
final bool isLoading;
final Exception? error;
AppConfigState copyWith({
String? hetznerKey,
String? cloudFlareKey,
BackblazeCredential? backblazeCredential,
CloudFlareDomain? cloudFlareDomain,
User? rootUser,
HetznerServerDetails? hetznerServer,
bool? isServerStarted,
bool? isServerResetedFirstTime,
bool? isServerResetedSecondTime,
bool? hasFinalChecked,
bool? isLoading,
Exception? error,
}) =>
AppConfigState(
hetznerKey: hetznerKey ?? this.hetznerKey,
cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey,
backblazeCredential: backblazeCredential ?? this.backblazeCredential,
cloudFlareDomain: cloudFlareDomain ?? this.cloudFlareDomain,
rootUser: rootUser ?? this.rootUser,
hetznerServer: hetznerServer ?? this.hetznerServer,
isServerStarted: isServerStarted ?? this.isServerStarted,
isServerResetedFirstTime:
isServerResetedFirstTime ?? this.isServerResetedFirstTime,
isServerResetedSecondTime:
isServerResetedSecondTime ?? this.isServerResetedSecondTime,
hasFinalChecked: hasFinalChecked ?? this.hasFinalChecked,
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
);
bool get isHetznerFilled => hetznerKey != null;
bool get isCloudFlareFilled => cloudFlareKey != null;
bool get isBackblazeFilled => backblazeCredential != null;
@ -84,9 +42,19 @@ class AppConfigState extends Equatable {
bool get isUserFilled => rootUser != null;
bool get isServerCreated => hetznerServer != null;
bool get isFullyInitilized => _fulfilementList.every((el) => el!);
// bool get isFullyInitilized => _fulfilementList.every((el) => el!);
int get progress => _fulfilementList.where((el) => el!).length;
int get porgressBar {
if (progress < 6) {
return progress;
} else if (progress < 10) {
return 6;
} else {
return 7;
}
}
List<bool?> get _fulfilementList {
var res = [
isHetznerFilled,
@ -98,32 +66,13 @@ class AppConfigState extends Equatable {
isServerStarted,
isServerResetedFirstTime,
isServerResetedSecondTime,
hasFinalChecked,
];
return res;
}
}
class InitialAppConfigState extends AppConfigState {
InitialAppConfigState()
: super(
hetznerKey: null,
cloudFlareKey: null,
backblazeCredential: null,
cloudFlareDomain: null,
rootUser: null,
hetznerServer: null,
isServerStarted: false,
isServerResetedFirstTime: false,
isServerResetedSecondTime: false,
hasFinalChecked: false,
isLoading: false,
error: null,
);
}
class TimerState extends AppConfigState {
class TimerState extends AppConfigNotFinished {
TimerState({
required this.dataState,
this.timerStart,
@ -139,12 +88,10 @@ class TimerState extends AppConfigState {
isServerStarted: dataState.isServerStarted,
isServerResetedFirstTime: dataState.isServerResetedFirstTime,
isServerResetedSecondTime: dataState.isServerResetedSecondTime,
hasFinalChecked: dataState.hasFinalChecked,
isLoading: isLoading,
error: dataState.error,
);
final AppConfigState dataState;
final AppConfigNotFinished dataState;
final DateTime? timerStart;
final Duration? duration;
@ -155,3 +102,134 @@ class TimerState extends AppConfigState {
duration,
];
}
class AppConfigNotFinished extends AppConfigState {
final bool isLoading;
AppConfigNotFinished({
String? hetznerKey,
String? cloudFlareKey,
BackblazeCredential? backblazeCredential,
CloudFlareDomain? cloudFlareDomain,
User? rootUser,
HetznerServerDetails? hetznerServer,
required bool isServerStarted,
required bool isServerResetedFirstTime,
required bool isServerResetedSecondTime,
required this.isLoading,
}) : super(
hetznerKey: hetznerKey,
cloudFlareKey: cloudFlareKey,
backblazeCredential: backblazeCredential,
cloudFlareDomain: cloudFlareDomain,
rootUser: rootUser,
hetznerServer: hetznerServer,
isServerStarted: isServerStarted,
isServerResetedFirstTime: isServerResetedFirstTime,
isServerResetedSecondTime: isServerResetedSecondTime,
);
@override
List<Object?> get props => [
hetznerKey,
cloudFlareKey,
backblazeCredential,
cloudFlareDomain,
rootUser,
hetznerServer,
isServerStarted,
isServerResetedFirstTime,
isLoading
];
AppConfigNotFinished copyWith({
String? hetznerKey,
String? cloudFlareKey,
BackblazeCredential? backblazeCredential,
CloudFlareDomain? cloudFlareDomain,
User? rootUser,
HetznerServerDetails? hetznerServer,
bool? isServerStarted,
bool? isServerResetedFirstTime,
bool? isServerResetedSecondTime,
bool? isLoading,
}) =>
AppConfigNotFinished(
hetznerKey: hetznerKey ?? this.hetznerKey,
cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey,
backblazeCredential: backblazeCredential ?? this.backblazeCredential,
cloudFlareDomain: cloudFlareDomain ?? this.cloudFlareDomain,
rootUser: rootUser ?? this.rootUser,
hetznerServer: hetznerServer ?? this.hetznerServer,
isServerStarted: isServerStarted ?? this.isServerStarted,
isServerResetedFirstTime:
isServerResetedFirstTime ?? this.isServerResetedFirstTime,
isServerResetedSecondTime:
isServerResetedSecondTime ?? this.isServerResetedSecondTime,
isLoading: isLoading ?? this.isLoading,
);
AppConfigFinished finish() => AppConfigFinished(
hetznerKey: hetznerKey!,
cloudFlareKey: cloudFlareKey!,
backblazeCredential: backblazeCredential!,
cloudFlareDomain: cloudFlareDomain!,
rootUser: rootUser!,
hetznerServer: hetznerServer!,
isServerStarted: isServerStarted,
isServerResetedFirstTime: isServerResetedFirstTime,
isServerResetedSecondTime: isServerResetedSecondTime,
);
}
class AppConfigEmpty extends AppConfigNotFinished {
AppConfigEmpty()
: super(
hetznerKey: null,
cloudFlareKey: null,
backblazeCredential: null,
cloudFlareDomain: null,
rootUser: null,
hetznerServer: null,
isServerStarted: false,
isServerResetedFirstTime: false,
isServerResetedSecondTime: false,
isLoading: false,
);
}
class AppConfigFinished extends AppConfigState {
const AppConfigFinished({
required String hetznerKey,
required String cloudFlareKey,
required BackblazeCredential backblazeCredential,
required CloudFlareDomain cloudFlareDomain,
required User rootUser,
required HetznerServerDetails hetznerServer,
required bool isServerStarted,
required bool isServerResetedFirstTime,
required bool isServerResetedSecondTime,
}) : super(
hetznerKey: hetznerKey,
cloudFlareKey: cloudFlareKey,
backblazeCredential: backblazeCredential,
cloudFlareDomain: cloudFlareDomain,
rootUser: rootUser,
hetznerServer: hetznerServer,
isServerStarted: isServerStarted,
isServerResetedFirstTime: isServerResetedFirstTime,
isServerResetedSecondTime: isServerResetedSecondTime,
);
@override
List<Object?> get props => [
hetznerKey,
cloudFlareKey,
backblazeCredential,
cloudFlareDomain,
rootUser,
hetznerServer,
isServerStarted,
isServerResetedFirstTime,
];
}

View File

@ -0,0 +1,39 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
export 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
part 'authentication_dependend_state.dart';
abstract class AppConfigDependendCubit<T extends AppConfigDependendState>
extends Cubit<T> {
AppConfigDependendCubit(
this.appConfigCubit,
T initState,
) : super(initState) {
authCubitSubscription = appConfigCubit.stream.listen(checkAuthStatus);
checkAuthStatus(appConfigCubit.state);
}
void checkAuthStatus(AppConfigState state) {
if (state is AppConfigFinished) {
load();
} else if (state is AppConfigEmpty) {
clear();
}
}
late StreamSubscription authCubitSubscription;
final AppConfigCubit appConfigCubit;
void load();
void clear();
@override
Future<void> close() {
authCubitSubscription.cancel();
return super.close();
}
}

View File

@ -0,0 +1,5 @@
part of 'authentication_dependend_cubit.dart';
abstract class AppConfigDependendState extends Equatable {
const AppConfigDependendState();
}

View File

@ -3,6 +3,7 @@ import 'package:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/api_maps/backblaze.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/models/backblaze_credential.dart';
import 'package:easy_localization/easy_localization.dart';
class BackblazeFormCubit extends FormCubit {
BackblazeFormCubit(this.initializingCubit) {
@ -10,7 +11,7 @@ class BackblazeFormCubit extends FormCubit {
keyId = FieldCubit(
initalValue: '',
validations: [
RequiredStringValidation('required'),
RequiredStringValidation('validations.required'.tr()),
//ValidationModel<String>(
//(s) => regExp.hasMatch(s), 'invalid key format'),
//LegnthStringValidationWithLenghShowing(64, 'length is [] shoud be 64')

View File

@ -4,6 +4,7 @@ import 'package:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/api_maps/cloudflare.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart';
import 'package:easy_localization/easy_localization.dart';
class CloudFlareFormCubit extends FormCubit {
CloudFlareFormCubit(this.initializingCubit) {
@ -11,10 +12,11 @@ class CloudFlareFormCubit extends FormCubit {
apiKey = FieldCubit(
initalValue: '',
validations: [
RequiredStringValidation('required'),
RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>(
(s) => regExp.hasMatch(s), 'invalid key format'),
LegnthStringValidationWithLenghShowing(40, 'length is [] shoud be 40')
(s) => regExp.hasMatch(s), 'validations.key_format'.tr()),
LegnthStringValidationWithLenghShowing(
40, 'validations.length'.tr(args: ["40"]))
],
);

View File

@ -4,6 +4,7 @@ import 'package:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/api_maps/hetzner.dart';
import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:easy_localization/easy_localization.dart';
class HetznerFormCubit extends FormCubit {
HetznerFormCubit(this.initializingCubit) {
@ -11,10 +12,10 @@ class HetznerFormCubit extends FormCubit {
apiKey = FieldCubit(
initalValue: '',
validations: [
RequiredStringValidation('required'),
RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>(
(s) => regExp.hasMatch(s), 'invalid key format'),
LegnthStringValidationWithLenghShowing(64, 'length is [] shoud be 64')
(s) => regExp.hasMatch(s), 'validations.key_format'.tr()),
LegnthStringValidationWithLenghShowing(64, 'validations.length'.tr(args: ["64"]))
],
);

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/models/user.dart';
import 'package:easy_localization/easy_localization.dart';
class RootUserFormCubit extends FormCubit {
RootUserFormCubit(this.initializingCubit) {
@ -12,18 +13,20 @@ class RootUserFormCubit extends FormCubit {
userName = FieldCubit(
initalValue: '',
validations: [
RequiredStringValidation('required'),
ValidationModel<String>(
(s) => userRegExp.hasMatch(s), 'invalid format'),
(s) => s.toLowerCase() == 'root', 'validations.root_name'.tr()),
RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>(
(s) => userRegExp.hasMatch(s), 'validations.invalid_format'.tr()),
],
);
password = FieldCubit(
initalValue: '',
validations: [
RequiredStringValidation('required'),
RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>(
(s) => passwordRegExp.hasMatch(s), 'invalid format'),
(s) => passwordRegExp.hasMatch(s), 'validations.invalid_format'.tr()),
],
);

View File

@ -1,13 +1,16 @@
import 'dart:async';
import 'package:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
import 'package:selfprivacy/logic/models/job.dart';
import 'package:selfprivacy/logic/models/user.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/utils/password_generator.dart';
class UserFormCubit extends FormCubit {
UserFormCubit({
required this.usersCubit,
required this.jobsCubit,
required List<User> users,
User? user,
}) {
var isEdit = user != null;
@ -18,18 +21,22 @@ class UserFormCubit extends FormCubit {
login = FieldCubit(
initalValue: isEdit ? user!.login : '',
validations: [
RequiredStringValidation('required'),
ValidationModel(
(login) => users.any((user) => user.login == login),
'validations.user_alredy_exist'.tr(),
),
RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>(
(s) => userRegExp.hasMatch(s), 'invalid format'),
(s) => userRegExp.hasMatch(s), 'validations.invalid_format'.tr()),
],
);
password = FieldCubit(
initalValue: isEdit ? user!.password : genPass(),
initalValue: isEdit ? user!.password : StringGenerators.userPassword(),
validations: [
RequiredStringValidation('required'),
ValidationModel<String>(
(s) => passwordRegExp.hasMatch(s), 'invalid format'),
RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>((s) => passwordRegExp.hasMatch(s),
'validations.invalid_format'.tr()),
],
);
@ -42,15 +49,15 @@ class UserFormCubit extends FormCubit {
login: login.state.value,
password: password.state.value,
);
usersCubit.addUser(user);
jobsCubit.addJob(CreateUserJob(user: user));
}
late FieldCubit<String> login;
late FieldCubit<String> password;
void genNewPassword() {
password.externalSetValue(genPass());
password.externalSetValue(StringGenerators.userPassword());
}
late UsersCubit usersCubit;
final JobsCubit jobsCubit;
}

View File

@ -6,7 +6,6 @@ import 'hetzner_metrics_cubit.dart';
class HetznerMetricsRepository {
Future<HetznerMetricsLoaded> getMetrics(Period period) async {
print(period);
var end = DateTime.now();
DateTime start;

View File

@ -0,0 +1,102 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/server.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
import 'package:selfprivacy/logic/get_it/ssh.dart';
import 'package:selfprivacy/logic/models/job.dart';
import 'package:equatable/equatable.dart';
import 'package:selfprivacy/logic/models/user.dart';
export 'package:provider/provider.dart';
import 'package:easy_localization/easy_localization.dart';
part 'jobs_state.dart';
class JobsCubit extends Cubit<JobsState> {
JobsCubit({
required this.usersCubit,
required this.servicesCubit,
}) : super(JobsStateEmpty());
final api = ServerApi();
final UsersCubit usersCubit;
final ServicesCubit servicesCubit;
void addJob(Job job) {
var newJobsList = <Job>[];
if (state is JobsStateWithJobs) {
newJobsList.addAll((state as JobsStateWithJobs).jobList);
}
newJobsList.add(job);
getIt<NavigationService>().showSnackBar('jobs.jobAdded'.tr());
emit(JobsStateWithJobs(newJobsList));
}
void removeJob(String id) {
final newState = (state as JobsStateWithJobs).removeById(id);
emit(newState);
}
void createOrRemoveServiceToggleJob(ServiceToggleJob job) {
var newJobsList = <Job>[];
if (state is JobsStateWithJobs) {
newJobsList.addAll((state as JobsStateWithJobs).jobList);
}
var needToRemoveJob =
newJobsList.any((el) => el is ServiceToggleJob && el.type == job.type);
if (needToRemoveJob) {
var removingJob = newJobsList
.firstWhere(((el) => el is ServiceToggleJob && el.type == job.type));
removeJob(removingJob.id);
} else {
newJobsList.add(job);
getIt<NavigationService>().showSnackBar('jobs.jobAdded'.tr());
emit(JobsStateWithJobs(newJobsList));
}
}
void createShhJobIfNotExist(CreateSSHKeyJob job) {
var newJobsList = <Job>[];
if (state is JobsStateWithJobs) {
newJobsList.addAll((state as JobsStateWithJobs).jobList);
}
var isExistInJobList = newJobsList.any((el) => el is CreateSSHKeyJob);
if (!isExistInJobList) {
newJobsList.add(job);
getIt<NavigationService>().showSnackBar('jobs.jobAdded'.tr());
emit(JobsStateWithJobs(newJobsList));
}
}
Future<void> applyAll() async {
if (state is JobsStateWithJobs) {
var jobs = (state as JobsStateWithJobs).jobList;
emit(JobsStateLoading());
var newUsers = <User>[];
var hasServiceJobs = false;
for (var job in jobs) {
if (job is CreateUserJob) {
newUsers.add(job.user);
await api.createUser(job.user);
} else if (job is ServiceToggleJob) {
hasServiceJobs = true;
await api.switchService(job.type, job.needToTurnOn);
}
if (job is CreateSSHKeyJob) {
await getIt<SSHModel>().generateKeys();
api.sendSsh(getIt<SSHModel>().savedPubKey!);
}
}
usersCubit.addUsers(newUsers);
await api.apply();
if (hasServiceJobs) {
await servicesCubit.load();
}
emit(JobsStateEmpty());
getIt<NavigationService>().navigator!.pop();
}
}
}

View File

@ -0,0 +1,27 @@
part of 'jobs_cubit.dart';
abstract class JobsState extends Equatable {
@override
List<Object?> get props => [];
}
class JobsStateLoading extends JobsState {}
class JobsStateEmpty extends JobsState {}
class JobsStateWithJobs extends JobsState {
JobsStateWithJobs(this.jobList);
final List<Job> jobList;
JobsState removeById(String id) {
var newJobsList = jobList.where((element) => element.id != id).toList();
if (newJobsList.isEmpty) {
return JobsStateEmpty();
}
return JobsStateWithJobs(newJobsList);
}
@override
List<Object?> get props => jobList;
}

View File

@ -0,0 +1,36 @@
import 'package:hive/hive.dart';
import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/logic/api_maps/server.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
part 'services_state.dart';
class ServicesCubit extends AppConfigDependendCubit<ServicesState> {
ServicesCubit(AppConfigCubit appConfigCubit)
: super(appConfigCubit, ServicesState.allOff());
Box box = Hive.box(BNames.servicesState);
final api = ServerApi();
Future<void> load() async {
if (appConfigCubit.state is AppConfigFinished) {
var statuses = await api.servicesPowerCheck();
emit(
ServicesState(
isPasswordManagerEnable: statuses[ServiceTypes.passwordManager]!,
isCloudEnable: statuses[ServiceTypes.cloud]!,
isGitEnable: statuses[ServiceTypes.git]!,
isSocialNetworkEnable: statuses[ServiceTypes.socialNetwork]!,
isVpnEnable: statuses[ServiceTypes.vpn]!,
),
);
}
}
@override
void clear() async {
box.clear();
emit(ServicesState.allOff());
}
}

View File

@ -0,0 +1,93 @@
part of 'services_cubit.dart';
class ServicesState extends AppConfigDependendState {
const ServicesState({
required this.isPasswordManagerEnable,
required this.isCloudEnable,
required this.isGitEnable,
required this.isSocialNetworkEnable,
required this.isVpnEnable,
});
final bool isPasswordManagerEnable;
final bool isCloudEnable;
final bool isGitEnable;
final bool isSocialNetworkEnable;
final bool isVpnEnable;
factory ServicesState.allOff() => ServicesState(
isPasswordManagerEnable: false,
isCloudEnable: false,
isGitEnable: false,
isSocialNetworkEnable: false,
isVpnEnable: false,
);
factory ServicesState.allOn() => ServicesState(
isPasswordManagerEnable: true,
isCloudEnable: true,
isGitEnable: true,
isSocialNetworkEnable: true,
isVpnEnable: true,
);
ServicesState enableList(
List<ServiceTypes> list,
) =>
ServicesState(
isPasswordManagerEnable: list.contains(ServiceTypes.passwordManager)
? true
: isPasswordManagerEnable,
isCloudEnable: list.contains(ServiceTypes.cloud) ? true : isCloudEnable,
isGitEnable:
list.contains(ServiceTypes.git) ? true : isPasswordManagerEnable,
isSocialNetworkEnable: list.contains(ServiceTypes.socialNetwork)
? true
: isPasswordManagerEnable,
isVpnEnable:
list.contains(ServiceTypes.vpn) ? true : isPasswordManagerEnable,
);
ServicesState disableList(
List<ServiceTypes> list,
) =>
ServicesState(
isPasswordManagerEnable: list.contains(ServiceTypes.passwordManager)
? false
: isPasswordManagerEnable,
isCloudEnable:
list.contains(ServiceTypes.cloud) ? false : isCloudEnable,
isGitEnable:
list.contains(ServiceTypes.git) ? false : isPasswordManagerEnable,
isSocialNetworkEnable: list.contains(ServiceTypes.socialNetwork)
? false
: isPasswordManagerEnable,
isVpnEnable:
list.contains(ServiceTypes.vpn) ? false : isPasswordManagerEnable,
);
@override
List<Object> get props => [
isPasswordManagerEnable,
isCloudEnable,
isGitEnable,
isSocialNetworkEnable,
isVpnEnable
];
bool isEnableByType(ServiceTypes type) {
switch (type) {
case ServiceTypes.passwordManager:
return isPasswordManagerEnable;
case ServiceTypes.cloud:
return isCloudEnable;
case ServiceTypes.socialNetwork:
return isSocialNetworkEnable;
case ServiceTypes.git:
return isGitEnable;
case ServiceTypes.vpn:
return isVpnEnable;
default:
throw Exception('wrong state');
}
}
}

View File

@ -1,42 +1,37 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:hive/hive.dart';
import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/logic/models/user.dart';
export 'package:provider/provider.dart';
part 'users_state.dart';
class UsersCubit extends Cubit<UsersState> {
UsersCubit() : super(UsersState([]));
UsersCubit() : super(UsersState(<User>[]));
Box<User> box = Hive.box<User>(BNames.users);
void addUser(User user) {
var users = [...state.users];
users.add(user);
emit(UsersState(users));
void load() async {
var loadedUsers = box.values.toList();
if (loadedUsers.isNotEmpty) {
emit(UsersState(loadedUsers));
}
}
void remove(User? user) {
void addUsers(List<User> users) async {
var newUserList = <User>[...state.users, ...users];
await box.addAll(users);
emit(UsersState(newUserList));
}
void remove(User user) async {
var users = [...state.users];
var index = users.indexOf(user);
users.remove(user);
await box.deleteAt(index);
emit(UsersState(users));
}
}
// final initMockUsers = <User>[
// User(login: 'Heartbreaking.Goose', password: genPass()),
// User(login: 'Alma.lawson', password: genPass()),
// User(login: 'Bee.gees', password: genPass()),
// User(login: 'Bim.jennings', password: genPass()),
// User(login: 'Debra.holt', password: genPass()),
// User(login: 'Georgia.young', password: genPass()),
// User(login: 'Kenzi.lawson', password: genPass()),
// User(login: 'Le.jennings', password: genPass()),
// User(login: 'Kirill.Zh', password: genPass()),
// User(login: 'Tina.Bolton', password: genPass()),
// User(login: 'Rebekah.Lynn', password: genPass()),
// User(login: 'Aleena.Armstrong', password: genPass()),
// User(login: 'Rosemary.Williams', password: genPass()),
// User(login: 'Sullivan.Nixon', password: genPass()),
// User(login: 'Aleena.Armstrong', password: genPass()),
// ];

View File

@ -1,8 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/text_themes.dart';
class NavigationService {
final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
NavigatorState? get navigator => navigatorKey.currentState;
void showPopUpDialog(AlertDialog dialog) {
@ -13,4 +18,14 @@ class NavigationService {
builder: (_) => dialog,
);
}
void showSnackBar(String text) {
final state = scaffoldMessengerKey.currentState!;
final snack = SnackBar(
backgroundColor: BrandColors.black.withOpacity(0.8),
content: Text(text, style: buttonTitleText),
duration: const Duration(seconds: 2),
);
state.showSnackBar(snack);
}
}

39
lib/logic/get_it/ssh.dart Normal file
View File

@ -0,0 +1,39 @@
import 'package:hive/hive.dart';
import 'package:pointycastle/pointycastle.dart';
import 'package:rsa_encrypt/rsa_encrypt.dart';
import 'package:selfprivacy/config/hive_config.dart';
import 'package:pointycastle/api.dart' as crypto;
import 'package:ssh_key/ssh_key.dart' as ssh_key;
class SSHModel {
Box _box = Hive.box(BNames.sshConfig);
String? savedPrivateKey;
String? savedPubKey;
Future<void> generateKeys() async {
var helper = RsaKeyHelper();
crypto.AsymmetricKeyPair pair =
await helper.computeRSAKeyPair(helper.getSecureRandom());
var privateKey = pair.privateKey as RSAPrivateKey;
var publicKey = pair.publicKey as RSAPublicKey;
savedPrivateKey = helper.encodePrivateKeyToPemPKCS1(privateKey);
savedPubKey = publicKey.encode(ssh_key.PubKeyEncoding.openSsh);
await _box.put(BNames.sshPrivateKey, savedPrivateKey);
await _box.put(BNames.sshPublicKey, savedPubKey);
}
void init() async {
savedPrivateKey = _box.get(BNames.sshPrivateKey);
savedPubKey = _box.get(BNames.sshPublicKey);
}
bool get isSSHKeyGenerated => savedPrivateKey != null && savedPubKey != null;
Future<void> clear() async {
savedPrivateKey = null;
savedPubKey = null;
await _box.clear();
}
}

54
lib/logic/models/job.dart Normal file
View File

@ -0,0 +1,54 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/utils/password_generator.dart';
import 'package:easy_localization/easy_localization.dart';
import 'user.dart';
@immutable
class Job extends Equatable {
Job({
String? id,
required this.title,
}) : id = id ?? StringGenerators.simpleId();
final String title;
final String id;
@override
List<Object> get props => [id, title];
}
class CreateUserJob extends Job {
CreateUserJob({
required this.user,
}) : super(title: '${"jobs.createUser".tr()} ${user.login}');
final User user;
@override
List<Object> get props => [id, title, user];
}
class ServiceToggleJob extends Job {
ServiceToggleJob({
required this.type,
required this.needToTurnOn,
}) : super(
title:
'${needToTurnOn ? "jobs.serviceTurnOn".tr() : "jobs.serviceTurnOff".tr()} ${type.title}');
final ServiceTypes type;
final bool needToTurnOn;
@override
List<Object> get props => [id, title, type, needToTurnOn];
}
class CreateSSHKeyJob extends Job {
CreateSSHKeyJob() : super(title: '${"more.create_ssh_key".tr()}');
@override
List<Object> get props => [id, title];
}

View File

@ -4,6 +4,7 @@ import 'package:crypt/crypt.dart';
import 'package:equatable/equatable.dart';
import 'package:selfprivacy/utils/color_utils.dart';
import 'package:hive/hive.dart';
import 'package:selfprivacy/utils/password_generator.dart';
part 'user.g.dart';
@ -25,7 +26,10 @@ class User extends Equatable {
Color get color => stringToColor(login);
Crypt get hashPassword => Crypt.sha512(password);
Crypt get hashPassword => Crypt.sha512(
password,
salt: StringGenerators.passwordSalt(),
);
String toString() {
return login;

View File

@ -16,47 +16,51 @@ import 'config/localization.dart';
import 'logic/cubit/app_settings/app_settings_cubit.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await HiveConfig.init();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
Bloc.observer = SimpleBlocObserver();
Wakelock.enable();
await getItSetup();
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
runApp(
Localization(
child: BlocAndProviderConfig(
child: MyApp(),
),
),
);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
AppSettingsState appSettings = context.watch<AppSettingsCubit>().state;
return Localization(
child: BlocAndProviderConfig(
child: Builder(builder: (context) {
var appSettings = context.watch<AppSettingsCubit>().state;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light, // Manually changnig appbar color
child: MaterialApp(
navigatorKey: getIt.get<NavigationService>().navigatorKey,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: false,
title: 'SelfPrivacy',
theme: appSettings.isDarkModeOn ? darkTheme : ligtTheme,
home: appSettings.isOnbordingShowing
? OnboardingPage(nextPage: InitializingPage())
: RootPage(),
builder: (BuildContext context, Widget? widget) {
Widget error = Text('...rendering error...');
if (widget is Scaffold || widget is Navigator)
error = Scaffold(body: Center(child: error));
ErrorWidget.builder = (FlutterErrorDetails errorDetails) => error;
return widget!;
},
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light, // Manually changnig appbar color
child: MaterialApp(
scaffoldMessengerKey:
getIt.get<NavigationService>().scaffoldMessengerKey,
navigatorKey: getIt.get<NavigationService>().navigatorKey,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: false,
title: 'SelfPrivacy',
theme: appSettings.isDarkModeOn ? darkTheme : ligtTheme,
home: appSettings.isOnbordingShowing
? OnboardingPage(nextPage: InitializingPage())
: RootPage(),
builder: (BuildContext context, Widget? widget) {
Widget error = Text('...rendering error...');
if (widget is Scaffold || widget is Navigator)
error = Scaffold(body: Center(child: error));
ErrorWidget.builder =
(FlutterErrorDetails errorDetails) => error;
return widget!;
},
),
);
}),
),
);
}

View File

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
class BrandBottomSheet extends StatelessWidget {
const BrandBottomSheet({
Key? key,
required this.child,
this.isExpended = false,
}) : super(key: key);
final Widget child;
final bool isExpended;
@override
Widget build(BuildContext context) {
var mainHeight = MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top -
100;
late Widget innerWidget;
if (isExpended) {
innerWidget = Scaffold(
body: child,
);
} else {
final ThemeData themeData = Theme.of(context);
innerWidget = Material(
color: themeData.scaffoldBackgroundColor,
child: IntrinsicHeight(child: child),
);
}
return ConstrainedBox(
constraints: BoxConstraints(maxHeight: mainHeight + 4 + 6),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Container(
height: 4,
width: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: BrandColors.gray4,
),
),
),
SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: mainHeight),
child: innerWidget,
),
),
],
),
);
}
}

View File

@ -10,14 +10,14 @@ class BrandButton {
static rised({
Key? key,
required VoidCallback? onPressed,
String? title,
String? text,
Widget? child,
}) {
assert(title == null || child == null, 'required title or child');
assert(title != null || child != null, 'required title or child');
assert(text == null || child == null, 'required title or child');
assert(text != null || child != null, 'required title or child');
return _RisedButton(
key: key,
title: title,
title: text,
onPressed: onPressed,
child: child,
);

View File

@ -1,29 +0,0 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/utils/extensions/elevation_extension.dart';
class BrandCard extends StatelessWidget {
const BrandCard({
Key? key,
this.child,
}) : super(key: key);
final Widget? child;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.dark
? BrandColors.black
: BrandColors.white,
borderRadius: BorderRadius.circular(20),
).ev8,
padding: EdgeInsets.symmetric(
horizontal: 20,
vertical: 15,
),
child: child,
);
}
}

View File

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
class BrandCards {
static Widget big({required Widget child}) => _BrandCard(
child: child,
padding: EdgeInsets.symmetric(
horizontal: 20,
vertical: 15,
),
shadow: bigShadow,
borderRadius: BorderRadius.circular(20),
);
static Widget small({required Widget child}) => _BrandCard(
child: child,
padding: EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
),
shadow: bigShadow,
borderRadius: BorderRadius.circular(10),
);
}
class _BrandCard extends StatelessWidget {
const _BrandCard({
Key? key,
required this.child,
required this.padding,
required this.shadow,
required this.borderRadius,
}) : super(key: key);
final Widget child;
final EdgeInsets padding;
final List<BoxShadow> shadow;
final BorderRadius borderRadius;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.dark
? BrandColors.black
: BrandColors.white,
borderRadius: borderRadius,
boxShadow: shadow,
),
padding: padding,
child: child,
);
}
}
final bigShadow = [
BoxShadow(
offset: Offset(0, 4),
blurRadius: 8,
color: Colors.black.withOpacity(.08),
)
];

View File

@ -1,16 +1,19 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/pre_styled_buttons/pre_styled_buttons.dart';
class BrandHeader extends StatelessWidget {
const BrandHeader({
Key? key,
required this.title,
this.hasBackButton = false,
this.hasFlashButton = false,
}) : super(key: key);
final String title;
final bool hasBackButton;
final bool hasFlashButton;
@override
Widget build(BuildContext context) {
@ -31,6 +34,8 @@ class BrandHeader extends StatelessWidget {
SizedBox(width: 10),
],
BrandText.h4(title),
Spacer(),
if (hasFlashButton) PreStyledButtons.flash(),
],
),
),

View File

@ -0,0 +1,21 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
class BrandLoader {
static horizontal() => _HorizontalLoader();
}
class _HorizontalLoader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('basis.wait'.tr()),
SizedBox(height: 10),
LinearProgressIndicator(minHeight: 3),
],
);
}
}

View File

@ -56,6 +56,7 @@ class _BrandMarkdownState extends State<BrandMarkdown> {
),
);
return Markdown(
shrinkWrap: true,
styleSheet: markdown,
onTapLink: (String text, String? href, String title) {
if (href != null) {

View File

@ -1,63 +1,63 @@
import 'package:flutter/material.dart';
// import 'package:flutter/material.dart';
var navigatorKey = GlobalKey<NavigatorState>();
// var navigatorKey = GlobalKey<NavigatorState>();
class BrandModalSheet extends StatelessWidget {
const BrandModalSheet({
Key? key,
this.child,
}) : super(key: key);
// class BrandModalSheet extends StatelessWidget {
// const BrandModalSheet({
// Key? key,
// this.child,
// }) : super(key: key);
final Widget? child;
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
minChildSize: 0.95,
initialChildSize: 1,
maxChildSize: 1,
builder: (context, scrollController) {
return SingleChildScrollView(
controller: scrollController,
physics: ClampingScrollPhysics(),
child: Container(
child: Column(
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
behavior: HitTestBehavior.opaque,
child: Container(
width: double.infinity,
child: Center(
child: Padding(
padding: EdgeInsets.only(top: 132, bottom: 6),
child: Container(
height: 4,
width: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: Color(0xFFE3E3E3).withOpacity(0.65),
),
),
),
),
),
),
Container(
constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height - 132,
maxHeight: MediaQuery.of(context).size.height - 132,
),
decoration: BoxDecoration(
borderRadius:
BorderRadius.vertical(top: Radius.circular(20)),
color: Theme.of(context).scaffoldBackgroundColor,
),
width: double.infinity,
child: child),
],
),
),
);
});
}
}
// final Widget? child;
// @override
// Widget build(BuildContext context) {
// return DraggableScrollableSheet(
// minChildSize: 1,
// initialChildSize: 1,
// maxChildSize: 1,
// builder: (context, scrollController) {
// return SingleChildScrollView(
// controller: scrollController,
// physics: ClampingScrollPhysics(),
// child: Container(
// child: Column(
// children: [
// GestureDetector(
// onTap: () => Navigator.of(context).pop(),
// behavior: HitTestBehavior.opaque,
// child: Container(
// width: double.infinity,
// child: Center(
// child: Padding(
// padding: EdgeInsets.only(top: 132, bottom: 6),
// child: Container(
// height: 4,
// width: 30,
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(2),
// color: Color(0xFFE3E3E3).withOpacity(0.65),
// ),
// ),
// ),
// ),
// ),
// ),
// Container(
// constraints: BoxConstraints(
// minHeight: MediaQuery.of(context).size.height - 132,
// maxHeight: MediaQuery.of(context).size.height - 132,
// ),
// decoration: BoxDecoration(
// borderRadius:
// BorderRadius.vertical(top: Radius.circular(20)),
// color: Theme.of(context).scaffoldBackgroundColor,
// ),
// width: double.infinity,
// child: child),
// ],
// ),
// ),
// );
// });
// }
// }

View File

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
class BrandSwitch extends StatelessWidget {
const BrandSwitch({
Key? key,
required this.onChanged,
required this.value,
}) : super(key: key);
final ValueChanged<bool> onChanged;
final bool value;
@override
Widget build(BuildContext context) {
return Switch(
activeColor: BrandColors.green1,
activeTrackColor: BrandColors.green2,
value: value,
onChanged: onChanged,
);
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/text_themes.dart';
export 'package:selfprivacy/utils/extensions/text_extensions.dart';
enum TextType {
h1, // right now only at onboarding and opened providers
@ -12,7 +13,8 @@ enum TextType {
medium,
small,
onboardingTitle,
buttonTitleText // risen button title text,
buttonTitleText, // risen button title text,
h4Underlined,
}
class BrandText extends StatelessWidget {
@ -24,6 +26,7 @@ class BrandText extends StatelessWidget {
this.overflow,
this.softWrap,
this.textAlign,
this.maxLines,
}) : super(key: key);
final String? text;
@ -32,6 +35,7 @@ class BrandText extends StatelessWidget {
final TextOverflow? overflow;
final bool? softWrap;
final TextAlign? textAlign;
final int? maxLines;
factory BrandText.h1(
String? text, {
@ -51,10 +55,16 @@ class BrandText extends StatelessWidget {
type: TextType.onboardingTitle,
style: style,
);
factory BrandText.h2(String? text, {TextStyle? style}) => BrandText(
factory BrandText.h2(
String? text, {
TextStyle? style,
TextAlign? textAlign,
}) =>
BrandText(
text,
type: TextType.h2,
style: style,
textAlign: textAlign,
);
factory BrandText.h3(String text, {TextStyle? style, TextAlign? textAlign}) =>
BrandText(
@ -73,6 +83,24 @@ class BrandText extends StatelessWidget {
text,
type: TextType.h4,
style: style,
softWrap: true,
overflow: TextOverflow.ellipsis,
maxLines: 2,
textAlign: textAlign,
);
factory BrandText.h4Underlined(
String? text, {
TextStyle? style,
TextAlign? textAlign,
}) =>
BrandText(
text,
type: TextType.h4Underlined,
style: style,
softWrap: true,
overflow: TextOverflow.ellipsis,
maxLines: 2,
textAlign: textAlign,
);
@ -120,7 +148,6 @@ class BrandText extends StatelessWidget {
Text build(BuildContext context) {
TextStyle style;
var isDark = Theme.of(context).brightness == Brightness.dark;
switch (type) {
case TextType.h1:
style = isDark
@ -142,6 +169,11 @@ class BrandText extends StatelessWidget {
? headline4Style.copyWith(color: Colors.white)
: headline4Style;
break;
case TextType.h4Underlined:
style = isDark
? headline4UnderlinedStyle.copyWith(color: Colors.white)
: headline4UnderlinedStyle;
break;
case TextType.h5:
style = isDark
? headline5Style.copyWith(color: Colors.white)
@ -179,6 +211,7 @@ class BrandText extends StatelessWidget {
return Text(
text!,
style: style,
maxLines: maxLines,
overflow: overflow,
softWrap: softWrap,
textAlign: textAlign,

View File

@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart';
import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
class JobsContent extends StatelessWidget {
const JobsContent({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<JobsCubit, JobsState>(
builder: (context, state) {
late final List<Widget> widgets;
if (state is JobsStateEmpty) {
widgets = [
SizedBox(height: 80),
Center(child: BrandText.body1('jobs.empty'.tr())),
];
} else if (state is JobsStateLoading) {
widgets = [
SizedBox(height: 80),
BrandLoader.horizontal(),
];
} else if (state is JobsStateWithJobs) {
widgets = [
...state.jobList
.map(
(j) => Row(
children: [
Expanded(
child: BrandCards.small(
child: Text(j.title),
),
),
SizedBox(width: 10),
ElevatedButton(
style: ElevatedButton.styleFrom(
primary: BrandColors.red1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () =>
context.read<JobsCubit>().removeJob(j.id),
child: Text('basis.remove'.tr()),
),
],
),
)
.toList(),
SizedBox(height: 20),
BrandButton.rised(
onPressed: () => context.read<JobsCubit>().applyAll(),
text: 'jobs.start'.tr(),
),
];
}
return ListView(
padding: paddingH15V0,
children: [
SizedBox(height: 15),
Center(
child: BrandText.h2(
'jobs.title'.tr(),
),
),
SizedBox(height: 20),
...widgets
],
);
},
);
}
}

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/ui/components/pre_styled_buttons.dart';
import 'package:selfprivacy/ui/components/pre_styled_buttons/pre_styled_buttons.dart';
class OnePage extends StatelessWidget {
const OnePage({
@ -16,33 +16,31 @@ class OnePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: PreferredSize(
child: Column(
children: [
Container(
height: 51,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 15),
child: BrandText.h4('basis.details'.tr()),
),
BrandDivider(),
],
),
preferredSize: Size.fromHeight(52),
),
body: child,
bottomNavigationBar: SafeArea(
child: Container(
decoration: BoxDecoration(boxShadow: kElevationToShadow[3]),
height: kBottomNavigationBarHeight,
child: Container(
color: Theme.of(context).scaffoldBackgroundColor,
return Scaffold(
appBar: PreferredSize(
child: Column(
children: [
Container(
height: 51,
alignment: Alignment.center,
child: PreStyledButtons.close(
onPress: () => Navigator.of(context).pop()),
padding: EdgeInsets.symmetric(horizontal: 15),
child: BrandText.h4('basis.details'.tr()),
),
BrandDivider(),
],
),
preferredSize: Size.fromHeight(52),
),
body: child,
bottomNavigationBar: SafeArea(
child: Container(
decoration: BoxDecoration(boxShadow: kElevationToShadow[3]),
height: kBottomNavigationBarHeight,
child: Container(
color: Theme.of(context).scaffoldBackgroundColor,
alignment: Alignment.center,
child: PreStyledButtons.close(
onPress: () => Navigator.of(context).pop()),
),
),
),

View File

@ -1,13 +1,4 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:easy_localization/easy_localization.dart';
class PreStyledButtons {
static Widget close({
required VoidCallback onPress,
}) =>
_CloseButton(onPress: onPress);
}
part of 'pre_styled_buttons.dart';
class _CloseButton extends StatelessWidget {
const _CloseButton({Key? key, required this.onPress}) : super(key: key);

View File

@ -0,0 +1,86 @@
part of 'pre_styled_buttons.dart';
class _BrandFlashButton extends StatefulWidget {
_BrandFlashButton({Key? key}) : super(key: key);
@override
_BrandFlashButtonState createState() => _BrandFlashButtonState();
}
class _BrandFlashButtonState extends State<_BrandFlashButton>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation _colorTween;
@override
void initState() {
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 800));
_colorTween = ColorTween(
begin: BrandColors.black,
end: BrandColors.primary,
).animate(_animationController);
super.initState();
WidgetsBinding.instance!.addPostFrameCallback(_afterLayout);
}
void _afterLayout(_) {
if (Theme.of(context).brightness == Brightness.dark) {
setState(() {
_colorTween = ColorTween(
begin: BrandColors.white,
end: BrandColors.primary,
).animate(_animationController);
});
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
bool wasPrevStateIsEmpty = true;
@override
Widget build(BuildContext context) {
return BlocListener<JobsCubit, JobsState>(
listener: (context, state) {
if (wasPrevStateIsEmpty && state is! JobsStateEmpty) {
wasPrevStateIsEmpty = false;
_animationController.forward();
} else if (!wasPrevStateIsEmpty && state is JobsStateEmpty) {
wasPrevStateIsEmpty = true;
_animationController.reverse();
}
},
child: IconButton(
onPressed: () {
showBrandBottomSheet(
context: context,
builder: (context) => BrandBottomSheet(
isExpended: true,
child: JobsContent(),
),
);
},
icon: AnimatedBuilder(
animation: _colorTween,
builder: (context, child) {
var v = _animationController.value;
var icon = v > 0.5 ? Ionicons.flash : Ionicons.flash_outline;
return Transform.scale(
scale: 1 + (v < 0.5 ? v : 1 - v) * 2,
child: Icon(
icon,
color: _colorTween.value,
),
);
}),
),
);
}
}

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ionicons/ionicons.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/ui/components/jobs_content/jobs_content.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
part 'close.dart';
part 'flash.dart';
class PreStyledButtons {
static Widget close({
required VoidCallback onPress,
}) =>
_CloseButton(onPress: onPress);
static Widget flash() => _BrandFlashButton();
}

View File

@ -53,9 +53,9 @@ class _ProgressBarState extends State<ProgressBar> {
width: 10,
),
);
even.add(
odd.add(
SizedBox(
width: 10,
width: 20,
),
);

View File

@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
Future<T?> showBrandBottomSheet<T>({
required BuildContext context,
required WidgetBuilder builder,
}) =>
showCupertinoModalBottomSheet<T>(
builder: builder,
barrierColor: Colors.black45,
context: context,
shadow: BoxShadow(color: Colors.transparent),
backgroundColor: Colors.transparent,
);

View File

@ -10,10 +10,10 @@ import 'package:selfprivacy/logic/cubit/forms/initializing/hetzner_form_cubit.da
import 'package:selfprivacy/logic/cubit/forms/initializing/root_user_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_card/brand_card.dart';
import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
import 'package:selfprivacy/ui/components/brand_modal_sheet/brand_modal_sheet.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart';
import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart';
@ -36,11 +36,11 @@ class InitializingPage extends StatelessWidget {
() => _stepCheck(cubit),
() => _stepCheck(cubit),
() => _stepCheck(cubit),
() => Container(child: Text('Everythigng is initialized'))
() => Container(child: Center(child: Text('initializing.finish'.tr())))
][cubit.state.progress]();
return BlocListener<AppConfigCubit, AppConfigState>(
listener: (context, state) {
if (state.isFullyInitilized) {
if (cubit.state is AppConfigFinished) {
Navigator.of(context).pushReplacement(materialRoute(RootPage()));
}
},
@ -50,7 +50,7 @@ class InitializingPage extends StatelessWidget {
child: Column(
children: [
Padding(
padding: brandPagePadding2.copyWith(top: 10, bottom: 10),
padding: paddingH15V0.copyWith(top: 10, bottom: 10),
child: ProgressBar(
steps: [
'Hetzner',
@ -59,12 +59,9 @@ class InitializingPage extends StatelessWidget {
'Domain',
'User',
'Server',
'',
'',
'',
'',
'✅ Check',
],
activeIndex: cubit.state.progress,
activeIndex: cubit.state.porgressBar,
),
),
_addCard(
@ -83,7 +80,7 @@ class InitializingPage extends StatelessWidget {
child: Container(
alignment: Alignment.center,
child: BrandButton.text(
title: cubit.state.isFullyInitilized
title: cubit.state is AppConfigFinished
? 'basis.close'.tr()
: 'basis.later'.tr(),
onPressed: () {
@ -132,7 +129,7 @@ class InitializingPage extends StatelessWidget {
onPressed: formCubitState.isSubmitting
? null
: () => context.read<HetznerFormCubit>().trySubmit(),
title: 'basis.connect'.tr(),
text: 'basis.connect'.tr(),
),
SizedBox(height: 10),
BrandButton.text(
@ -187,7 +184,7 @@ class InitializingPage extends StatelessWidget {
onPressed: formCubitState.isSubmitting
? null
: () => context.read<CloudFlareFormCubit>().trySubmit(),
title: 'basis.connect'.tr(),
text: 'basis.connect'.tr(),
),
SizedBox(height: 10),
BrandButton.text(
@ -238,7 +235,7 @@ class InitializingPage extends StatelessWidget {
onPressed: formCubitState.isSubmitting
? null
: () => context.read<BackblazeFormCubit>().trySubmit(),
title: 'basis.connect'.tr(),
text: 'basis.connect'.tr(),
),
SizedBox(height: 10),
BrandButton.text(
@ -330,7 +327,7 @@ class InitializingPage extends StatelessWidget {
SizedBox(height: 30),
BrandButton.rised(
onPressed: () => context.read<DomainSetupCubit>().saveDomain(),
title: 'initializing.10'.tr(),
text: 'initializing.10'.tr(),
),
],
SizedBox(height: 10),
@ -400,7 +397,7 @@ class InitializingPage extends StatelessWidget {
onPressed: formCubitState.isSubmitting
? null
: () => context.read<RootUserFormCubit>().trySubmit(),
title: 'basis.connect'.tr(),
text: 'basis.connect'.tr(),
),
SizedBox(height: 10),
BrandButton.text(
@ -414,7 +411,7 @@ class InitializingPage extends StatelessWidget {
}
Widget _stepServer(AppConfigCubit appConfigCubit) {
var isLoading = appConfigCubit.state.isLoading;
var isLoading = (appConfigCubit.state as AppConfigNotFinished).isLoading;
return Builder(builder: (context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -428,7 +425,7 @@ class InitializingPage extends StatelessWidget {
onPressed: isLoading
? null
: () => appConfigCubit.createServerAndSetDnsRecords(),
title: isLoading ? 'basis.loading'.tr() : 'initializing.11'.tr(),
text: isLoading ? 'basis.loading'.tr() : 'initializing.11'.tr(),
),
Spacer(flex: 2),
BrandButton.text(
@ -441,23 +438,31 @@ class InitializingPage extends StatelessWidget {
}
Widget _stepCheck(AppConfigCubit appConfigCubit) {
assert(appConfigCubit.state is TimerState, 'wronge state');
assert(appConfigCubit.state is AppConfigNotFinished, 'wronge state');
var state = appConfigCubit.state as TimerState;
late int doneCount;
late String? text;
if (state.isServerResetedSecondTime) {
text = 'initializing.13'.tr();
doneCount = 3;
} else if (state.isServerResetedFirstTime) {
text = 'initializing.21'.tr();
doneCount = 2;
} else if (state.isServerStarted) {
text = 'initializing.14'.tr();
doneCount = 1;
} else if (state.isServerCreated) {
text = 'initializing.15'.tr();
doneCount = 0;
}
return Builder(builder: (context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 15),
BrandText.h4(
'initializing.checks'.tr(args: [doneCount.toString(), "4"]),
),
Spacer(flex: 2),
SizedBox(height: 10),
BrandText.body2(text),
@ -488,8 +493,8 @@ class InitializingPage extends StatelessWidget {
Widget _addCard(Widget child) {
return Container(
height: 450,
padding: brandPagePadding2,
child: BrandCard(child: child),
padding: paddingH15V0,
child: BrandCards.big(child: child),
);
}
}
@ -501,12 +506,14 @@ class _HowHetzner extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BrandModalSheet(
return BrandBottomSheet(
isExpended: true,
child: Padding(
padding: brandPagePadding2.copyWith(top: 25),
child: BrandMarkdown(
fileName: 'how_hetzner',
)),
padding: paddingH15V0,
child: BrandMarkdown(
fileName: 'how_hetzner',
),
),
);
}
}

View File

@ -7,6 +7,7 @@ import 'package:selfprivacy/ui/components/action_button/action_button.dart';
import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart';
import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_switch/brand_switch.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/utils/named_font_weight.dart';
import 'package:easy_localization/easy_localization.dart';
@ -32,7 +33,7 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
preferredSize: Size.fromHeight(52),
),
body: ListView(
padding: brandPagePadding2,
padding: paddingH15V0,
children: [
BrandDivider(),
Container(
@ -52,9 +53,7 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
),
),
SizedBox(width: 5),
Switch(
activeColor: BrandColors.green1,
activeTrackColor: BrandColors.green2,
BrandSwitch(
value: Theme.of(context).brightness == Brightness.dark,
onChanged: (value) => context
.read<AppSettingsCubit>()
@ -120,69 +119,85 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
],
),
),
Container(
padding: EdgeInsets.only(top: 20, bottom: 5),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1, color: BrandColors.dividerColor),
)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: _TextColumn(
title: 'more.settings.5'.tr(),
value: 'more.settings.6'.tr(),
),
),
SizedBox(width: 5),
ElevatedButton(
style: ElevatedButton.styleFrom(
primary: BrandColors.red1,
),
child: Text(
'basis.delete'.tr(),
style: TextStyle(
color: BrandColors.white,
fontWeight: NamedFontWeight.demiBold,
),
),
onPressed: () {
showDialog(
context: context,
builder: (_) {
return BrandAlert(
title: 'modals.3'.tr(),
contentText: 'modals.6'.tr(),
acitons: [
ActionButton(
text: 'modals.7'.tr(),
isRed: true,
onPressed: () async {
await context
.read<AppConfigCubit>()
.serverDelete();
Navigator.of(context).pop();
}),
ActionButton(
text: 'basis.cancel'.tr(),
),
],
);
},
);
},
),
],
),
)
deleteServer(context)
],
),
);
}),
);
}
Widget deleteServer(BuildContext context) {
var isDisabled =
context.watch<AppConfigCubit>().state.hetznerServer == null;
return Container(
padding: EdgeInsets.only(top: 20, bottom: 5),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1, color: BrandColors.dividerColor),
)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: _TextColumn(
title: 'more.settings.5'.tr(),
value: 'more.settings.6'.tr(),
),
),
SizedBox(width: 5),
ElevatedButton(
style: ElevatedButton.styleFrom(
primary: BrandColors.red1,
),
child: Text(
'basis.delete'.tr(),
style: TextStyle(
color: BrandColors.white,
fontWeight: NamedFontWeight.demiBold,
),
),
onPressed: isDisabled
? null
: () {
showDialog(
context: context,
builder: (_) {
return BrandAlert(
title: 'modals.3'.tr(),
contentText: 'modals.6'.tr(),
acitons: [
ActionButton(
text: 'modals.7'.tr(),
isRed: true,
onPressed: () async {
showDialog(
context: context,
builder: (context) {
return Container(
alignment: Alignment.center,
child: CircularProgressIndicator(),
);
});
await context
.read<AppConfigCubit>()
.serverDelete();
Navigator.of(context).pop();
}),
ActionButton(
text: 'basis.cancel'.tr(),
),
],
);
},
);
},
),
],
),
);
}
}
class _TextColumn extends StatelessWidget {

View File

@ -18,7 +18,7 @@ class InfoPage extends StatelessWidget {
preferredSize: Size.fromHeight(52),
),
body: ListView(
padding: brandPagePadding2,
padding: paddingH15V0,
children: [
BrandDivider(),
SizedBox(height: 10),

View File

@ -1,15 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:ionicons/ionicons.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/config/text_themes.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
import 'package:selfprivacy/logic/get_it/ssh.dart';
import 'package:selfprivacy/logic/models/job.dart';
import 'package:selfprivacy/logic/models/state_types.dart';
import 'package:selfprivacy/ui/components/action_button/action_button.dart';
import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart';
import 'package:selfprivacy/ui/pages/initializing/initializing.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
import 'package:selfprivacy/ui/pages/rootRoute.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:share_plus/share_plus.dart';
import 'about/about.dart';
import 'app_settings/app_setting.dart';
@ -21,15 +35,21 @@ class MorePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var jobsCubit = context.watch<JobsCubit>();
var isReady = context.watch<AppConfigCubit>().state is AppConfigFinished;
return Scaffold(
appBar: PreferredSize(
child: BrandHeader(title: 'basis.more'.tr()),
child: BrandHeader(
title: 'basis.more'.tr(),
hasFlashButton: true,
),
preferredSize: Size.fromHeight(52),
),
body: ListView(
children: [
Padding(
padding: brandPagePadding2,
padding: paddingH15V0,
child: Column(
children: [
BrandDivider(),
@ -63,6 +83,73 @@ class MorePage extends StatelessWidget {
iconData: BrandIcons.terminal,
goTo: Console(),
),
_MoreMenuTapItem(
title: 'more.create_ssh_key'.tr(),
iconData: Ionicons.key_outline,
onTap: isReady
? () {
if (getIt<SSHModel>().isSSHKeyGenerated) {
showDialog<void>(
context: context,
builder: (BuildContext context) {
return _SSHExitsDetails(
onShareTap: () {
Share.share(
getIt<SSHModel>().savedPrivateKey!);
},
onDeleteTap: () {
showDialog(
context: context,
builder: (_) {
return BrandAlert(
title: 'modals.3'.tr(),
contentText:
'more.delete_ssh_text'.tr(),
acitons: [
ActionButton(
text: 'more.yes_delete'.tr(),
isRed: true,
onPressed: () {
getIt<SSHModel>().clear();
Navigator.of(context).pop();
}),
ActionButton(
text: 'basis.cancel'.tr(),
),
],
);
},
);
},
onCopyTap: () {
Clipboard.setData(ClipboardData(
text: getIt<SSHModel>()
.savedPrivateKey!));
getIt<NavigationService>()
.showSnackBar('more.copied_ssh'.tr());
},
);
},
);
} else {
showDialog<void>(
context: context,
builder: (BuildContext context) {
return _MoreDetails(
title: 'more.create_ssh_key'.tr(),
icon: Ionicons.key_outline,
onTap: () {
jobsCubit.createShhJobIfNotExist(
CreateSSHKeyJob());
},
text: 'more.generate_key_text'.tr(),
);
},
);
}
}
: null,
),
],
),
)
@ -72,6 +159,150 @@ class MorePage extends StatelessWidget {
}
}
class _SSHExitsDetails extends StatelessWidget {
const _SSHExitsDetails({
Key? key,
required this.onDeleteTap,
required this.onShareTap,
required this.onCopyTap,
}) : super(key: key);
final Function onDeleteTap;
final Function onShareTap;
final Function onCopyTap;
@override
Widget build(BuildContext context) {
var textStyle = body1Style.copyWith(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: BrandColors.black);
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: SingleChildScrollView(
child: Container(
width: 350,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: paddingH15V30,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 10),
Text(
'more.ssh_key_exist_text'.tr(),
style: textStyle,
),
SizedBox(height: 10),
Container(
child: BrandButton.text(
onPressed: () {
Navigator.of(context).pop();
onShareTap();
},
title: 'more.share'.tr(),
),
),
Container(
alignment: Alignment.centerLeft,
child: BrandButton.text(
onPressed: () {
Navigator.of(context).pop();
onDeleteTap();
},
title: 'basis.delete'.tr(),
),
),
Container(
child: BrandButton.text(
onPressed: () {
Navigator.of(context).pop();
onCopyTap();
},
title: 'more.copy_buffer'.tr(),
),
),
],
),
)
],
),
),
),
);
}
}
class _MoreDetails extends StatelessWidget {
const _MoreDetails({
Key? key,
required this.icon,
required this.title,
required this.onTap,
required this.text,
}) : super(key: key);
final String title;
final IconData icon;
final Function onTap;
final String text;
@override
Widget build(BuildContext context) {
var textStyle = body1Style.copyWith(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: BrandColors.black);
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: SingleChildScrollView(
child: Container(
width: 350,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: paddingH15V30,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconStatusMask(
status: StateType.stable,
child: Icon(icon, size: 40, color: Colors.white),
),
SizedBox(height: 10),
BrandText.h2(title),
SizedBox(height: 10),
Text(
text,
style: textStyle,
),
SizedBox(height: 40),
Center(
child: Container(
child: BrandButton.rised(
onPressed: () {
Navigator.of(context).pop();
onTap();
},
text: 'more.generate_key'.tr(),
),
),
),
],
),
)
],
),
),
),
);
}
}
class _NavItem extends StatelessWidget {
const _NavItem({
Key? key,
@ -88,29 +319,81 @@ class _NavItem extends StatelessWidget {
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => Navigator.of(context).push(materialRoute(goTo)),
child: Container(
padding: EdgeInsets.symmetric(vertical: 24),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1.0,
color: BrandColors.dividerColor,
),
),
),
child: Row(
children: [
BrandText.body1(title),
Spacer(),
SizedBox(
width: 56,
child: Icon(
iconData,
size: 20,
),
),
],
),
child: _MoreMenuItem(
iconData: iconData,
title: title,
isActive: true,
),
);
}
}
class _MoreMenuTapItem extends StatelessWidget {
const _MoreMenuTapItem({
Key? key,
required this.iconData,
required this.onTap,
required this.title,
}) : super(key: key);
final IconData iconData;
final VoidCallback? onTap;
final String title;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: _MoreMenuItem(
isActive: onTap != null,
iconData: iconData,
title: title,
),
);
}
}
class _MoreMenuItem extends StatelessWidget {
const _MoreMenuItem({
Key? key,
required this.iconData,
required this.title,
required this.isActive,
}) : super(key: key);
final IconData iconData;
final String title;
final bool isActive;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 24),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1.0,
color: BrandColors.dividerColor,
),
),
),
child: Row(
children: [
BrandText.body1(
title,
style: TextStyle(
color: isActive ? null : Colors.grey,
),
),
Spacer(),
SizedBox(
width: 56,
child: Icon(
iconData,
size: 20,
color: isActive ? null : Colors.grey,
),
),
],
),
);
}

View File

@ -79,7 +79,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
curve: Curves.easeIn,
);
},
title: 'basis.next'.tr(),
text: 'basis.next'.tr(),
),
SizedBox(height: 30),
],
@ -129,7 +129,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
(route) => false,
);
},
title: 'basis.got_it'.tr(),
text: 'basis.got_it'.tr(),
),
SizedBox(height: 30),
],

View File

@ -1,20 +1,19 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
import 'package:selfprivacy/logic/models/provider.dart';
import 'package:selfprivacy/logic/models/state_types.dart';
import 'package:selfprivacy/ui/components/brand_card/brand_card.dart';
import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart';
import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_modal_sheet/brand_modal_sheet.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart';
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
import 'package:selfprivacy/ui/components/one_page/one_page.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
import 'package:selfprivacy/ui/pages/server_details/server_details.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/utils/route_transitions/slide_bottom.dart';
import 'package:selfprivacy/utils/ui_helpers.dart';
var navigatorKey = GlobalKey<NavigatorState>();
@ -29,7 +28,7 @@ class ProvidersPage extends StatefulWidget {
class _ProvidersPageState extends State<ProvidersPage> {
@override
Widget build(BuildContext context) {
var isReady = context.watch<AppConfigCubit>().state.isFullyInitilized;
var isReady = context.watch<AppConfigCubit>().state is AppConfigFinished;
final cards = ProviderType.values
.map(
@ -46,11 +45,14 @@ class _ProvidersPageState extends State<ProvidersPage> {
.toList();
return Scaffold(
appBar: PreferredSize(
child: BrandHeader(title: 'providers.page_title'.tr()),
child: BrandHeader(
title: 'providers.page_title'.tr(),
hasFlashButton: true,
),
preferredSize: Size.fromHeight(52),
),
body: ListView(
padding: brandPagePadding2,
padding: paddingH15V0,
children: [
if (!isReady) ...[
NotReadyCard(),
@ -73,7 +75,7 @@ class _Card extends StatelessWidget {
String? message;
late String stableText;
late VoidCallback onTap;
var isReady = context.watch<AppConfigCubit>().state is AppConfigFinished;
AppConfigState appConfig = context.watch<AppConfigCubit>().state;
var domainName =
@ -83,24 +85,22 @@ class _Card extends StatelessWidget {
case ProviderType.server:
title = 'providers.server.card_title'.tr();
stableText = 'providers.server.status'.tr();
onTap = () => Navigator.of(context).push(
SlideBottomRoute(
OnePage(
title: title,
child: ServerDetails(),
),
onTap = () => showBrandBottomSheet(
context: context,
builder: (context) => BrandBottomSheet(
isExpended: true,
child: ServerDetails(),
),
);
break;
case ProviderType.domain:
title = 'providers.domain.card_title'.tr();
message = domainName;
stableText = 'providers.domain.status'.tr();
onTap = () => showModalBottomSheet<void>(
onTap = () => showBrandBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return _ProviderDetails(
provider: provider,
@ -127,8 +127,8 @@ class _Card extends StatelessWidget {
break;
}
return GestureDetector(
onTap: onTap,
child: BrandCard(
onTap: isReady ? onTap : null,
child: BrandCards.big(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -178,8 +178,7 @@ class _ProviderDetails extends StatelessWidget {
children = [
BrandText.body1('providers.domain.bottom_sheet.1'.tr()),
SizedBox(height: 10),
BrandText.body1(
'providers.domain.bottom_sheet.2'.tr(args: [domainName, 'Date'])),
BrandText.body1(domainName),
SizedBox(height: 10),
BrandText.body1('providers.domain.status'.tr()),
];
@ -196,37 +195,31 @@ class _ProviderDetails extends StatelessWidget {
];
break;
}
return BrandModalSheet(
child: Navigator(
key: navigatorKey,
initialRoute: '/',
onGenerateRoute: (_) {
return materialRoute(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 40),
Padding(
padding: brandPagePadding2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconStatusMask(
status: provider.state,
child:
Icon(provider.icon, size: 40, color: Colors.white),
),
SizedBox(height: 10),
BrandText.h1(title),
SizedBox(height: 10),
...children
],
return BrandBottomSheet(
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 40),
Padding(
padding: paddingH15V0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconStatusMask(
status: provider.state,
child: Icon(provider.icon, size: 40, color: Colors.white),
),
)
],
),
);
},
SizedBox(height: 10),
BrandText.h1(title),
SizedBox(height: 10),
...children,
SizedBox(height: 30),
],
),
)
],
),
),
);
}

View File

@ -13,7 +13,7 @@ class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
padding: brandPagePadding2,
padding: paddingH15V0,
children: [
SizedBox(height: 10),
BrandHeader(title: 'basis.settings'.tr(), hasBackButton: true),

View File

@ -51,7 +51,7 @@ class CpuChart extends StatelessWidget {
interval: 20,
rotateAngle: 90.0,
showTitles: true,
getTextStyles: (value) => const TextStyle(
getTextStyles: (_, __) => const TextStyle(
fontSize: 10,
color: Colors.purple,
fontWeight: FontWeight.bold,
@ -60,7 +60,7 @@ class CpuChart extends StatelessWidget {
return bottomTitle(value.toInt());
}),
leftTitles: SideTitles(
getTextStyles: (value) => progressTextStyleLight.copyWith(
getTextStyles: (_, __) => progressTextStyleLight.copyWith(
color: Theme.of(context).brightness == Brightness.dark
? BrandColors.gray4
: null,
@ -82,7 +82,6 @@ class CpuChart extends StatelessWidget {
double appliedInterval,
double value,
) {
print(value);
if (value < 0) {
return false;
} else if (value == 0) {

View File

@ -71,7 +71,7 @@ class NetworkChart extends StatelessWidget {
interval: 20,
rotateAngle: 90.0,
showTitles: true,
getTextStyles: (value) => const TextStyle(
getTextStyles: (_, __) => const TextStyle(
fontSize: 10,
color: Colors.purple,
fontWeight: FontWeight.bold,
@ -87,7 +87,7 @@ class NetworkChart extends StatelessWidget {
].reduce(max) *
1.2 /
10,
getTextStyles: (value) => progressTextStyleLight.copyWith(
getTextStyles: (_, __) => progressTextStyleLight.copyWith(
color: Theme.of(context).brightness == Brightness.dark
? BrandColors.gray4
: null,

View File

@ -53,44 +53,61 @@ class _ServerDetailsState extends State<ServerDetails>
@override
Widget build(BuildContext context) {
var isReady = context.watch<AppConfigCubit>().state.isFullyInitilized;
var isReady = context.watch<AppConfigCubit>().state is AppConfigFinished;
var providerState = isReady ? StateType.stable : StateType.uninitialized;
return TabBarView(
physics: NeverScrollableScrollPhysics(),
controller: tabController,
children: [
SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: brandPagePadding2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_Header(
providerState: providerState,
tabController: tabController),
BrandText.body1('providers.server.bottom_sheet.1'.tr()),
SizedBox(height: 10),
BlocProvider(
create: (context) => HetznerMetricsCubit()..restart(),
child: _Chart(),
),
SizedBox(height: 20),
BlocProvider(
create: (context) => ServerDetailsCubit()..check(),
child: _TextDetails(),
),
],
),
),
],
),
return Scaffold(
appBar: PreferredSize(
child: Column(
children: [
Container(
height: 51,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 15),
child: BrandText.h4('basis.details'.tr()),
),
BrandDivider(),
],
),
_ServerSettings(tabController: tabController),
],
preferredSize: Size.fromHeight(52),
),
body: TabBarView(
physics: NeverScrollableScrollPhysics(),
controller: tabController,
children: [
SingleChildScrollView(
physics: ClampingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: paddingH15V0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_Header(
providerState: providerState,
tabController: tabController),
BrandText.body1('providers.server.bottom_sheet.1'.tr()),
SizedBox(height: 10),
BlocProvider(
create: (context) => HetznerMetricsCubit()..restart(),
child: _Chart(),
),
SizedBox(height: 20),
BlocProvider(
create: (context) => ServerDetailsCubit()..check(),
child: _TextDetails(),
),
],
),
),
],
),
),
_ServerSettings(tabController: tabController),
],
),
);
}
}

View File

@ -11,7 +11,7 @@ class _ServerSettings extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
padding: brandPagePadding2,
padding: paddingH15V0,
children: [
SizedBox(height: 10),
Container(

View File

@ -27,13 +27,13 @@ class _TextDetails extends StatelessWidget {
children: [
TableRow(
children: [
getRowTitle('Last check'),
getRowTitle('Last check:'),
getRowValue(formater.format(checkTime)),
],
),
TableRow(
children: [
getRowTitle('Server Id'),
getRowTitle('Server Id:'),
getRowValue(data.id.toString()),
],
),
@ -48,7 +48,7 @@ class _TextDetails extends StatelessWidget {
),
TableRow(
children: [
getRowTitle('CPU'),
getRowTitle('CPU:'),
getRowValue(
data.serverType.cores.toString(),
),
@ -56,7 +56,7 @@ class _TextDetails extends StatelessWidget {
),
TableRow(
children: [
getRowTitle('Memory'),
getRowTitle('Memory:'),
getRowValue(
'${data.serverType.memory.toString()} GB',
),
@ -64,7 +64,7 @@ class _TextDetails extends StatelessWidget {
),
TableRow(
children: [
getRowTitle('Disk Local'),
getRowTitle('Disk Local:'),
getRowValue(
'${data.serverType.disk.toString()} GB',
),
@ -100,7 +100,7 @@ class _TextDetails extends StatelessWidget {
children: [
TableRow(
children: [
getRowTitle('Country'),
getRowTitle('Country:'),
getRowValue(
'${data.location.country}',
),
@ -108,13 +108,13 @@ class _TextDetails extends StatelessWidget {
),
TableRow(
children: [
getRowTitle('City'),
getRowTitle('City:'),
getRowValue(data.location.city),
],
),
TableRow(
children: [
getRowTitle('Description'),
getRowTitle('Description:'),
getRowValue(data.location.description),
],
),

View File

@ -1,13 +1,19 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/config/text_themes.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/models/job.dart';
import 'package:selfprivacy/logic/models/state_types.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_card/brand_card.dart';
import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_switch/brand_switch.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart';
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
@ -17,6 +23,14 @@ import 'package:url_launcher/url_launcher.dart';
import '../rootRoute.dart';
const switchableServices = [
ServiceTypes.passwordManager,
ServiceTypes.cloud,
ServiceTypes.socialNetwork,
ServiceTypes.git,
ServiceTypes.vpn,
];
class ServicesPage extends StatefulWidget {
ServicesPage({Key? key}) : super(key: key);
@ -27,15 +41,18 @@ class ServicesPage extends StatefulWidget {
class _ServicesPageState extends State<ServicesPage> {
@override
Widget build(BuildContext context) {
var isReady = context.watch<AppConfigCubit>().state.isFullyInitilized;
var isReady = context.watch<AppConfigCubit>().state is AppConfigFinished;
return Scaffold(
appBar: PreferredSize(
child: BrandHeader(title: 'basis.services'.tr()),
child: BrandHeader(
title: 'basis.services'.tr(),
hasFlashButton: true,
),
preferredSize: Size.fromHeight(52),
),
body: ListView(
padding: brandPagePadding2,
padding: paddingH15V0,
children: [
BrandText.body1('services.title'.tr()),
SizedBox(height: 24),
@ -60,78 +77,115 @@ class _Card extends StatelessWidget {
final ServiceTypes serviceType;
@override
Widget build(BuildContext context) {
String title;
IconData iconData;
String subtitle;
switch (serviceType) {
case ServiceTypes.mail:
iconData = BrandIcons.envelope;
title = 'services.mail.title'.tr();
subtitle = 'services.mail.subtitle'.tr();
break;
case ServiceTypes.messenger:
iconData = BrandIcons.messanger;
title = 'services.messenger.title'.tr();
subtitle = 'services.messenger.subtitle'.tr();
break;
case ServiceTypes.passwordManager:
iconData = BrandIcons.key;
title = 'services.password_manager.title'.tr();
subtitle = 'services.password_manager.subtitle'.tr();
break;
case ServiceTypes.video:
iconData = BrandIcons.webcam;
title = 'services.video.title'.tr();
subtitle = 'services.video.subtitle'.tr();
break;
case ServiceTypes.cloud:
iconData = BrandIcons.upload;
title = 'services.cloud.title'.tr();
subtitle = 'services.cloud.subtitle'.tr();
break;
case ServiceTypes.socialNetwork:
iconData = BrandIcons.social;
title = 'services.social_network.title'.tr();
subtitle = 'services.social_network.subtitle'.tr();
break;
case ServiceTypes.git:
iconData = BrandIcons.git;
title = 'services.git.title'.tr();
subtitle = 'services.git.subtitle'.tr();
break;
}
var isReady = context.watch<AppConfigCubit>().state.isFullyInitilized;
var isReady = context.watch<AppConfigCubit>().state is AppConfigFinished;
var changeTab = context.read<ChangeTab>().onPress;
var serviceState = context.watch<ServicesCubit>().state;
var jobsCubit = context.watch<JobsCubit>();
var jobState = jobsCubit.state;
var switchebleService = switchableServices.contains(serviceType);
var hasSwitchJob = switchebleService &&
jobState is JobsStateWithJobs &&
jobState.jobList
.any((el) => el is ServiceToggleJob && el.type == serviceType);
var isSwithOn = isReady &&
(!switchableServices.contains(serviceType) ||
serviceState.isEnableByType(serviceType));
return GestureDetector(
onTap: () => showDialog<void>(
context: context,
// isScrollControlled: true,
// backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return _ServiceDetails(
serviceType: serviceType,
status: isReady ? StateType.stable : StateType.uninitialized,
title: title,
icon: iconData,
changeTab: changeTab,
);
},
),
child: BrandCard(
onTap: isSwithOn
? () => showDialog<void>(
context: context,
// isScrollControlled: true,
// backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return _ServiceDetails(
serviceType: serviceType,
status:
isSwithOn ? StateType.stable : StateType.uninitialized,
title: serviceType.title,
icon: serviceType.icon,
changeTab: changeTab,
);
},
)
: null,
child: BrandCards.big(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconStatusMask(
status: isReady ? StateType.stable : StateType.uninitialized,
child: Icon(iconData, size: 30, color: Colors.white),
Row(
children: [
IconStatusMask(
status:
isSwithOn ? StateType.stable : StateType.uninitialized,
child: Icon(serviceType.icon, size: 30, color: Colors.white),
),
if (isReady && switchebleService) ...[
Spacer(),
Builder(
builder: (context) {
late bool isActive;
if (hasSwitchJob) {
isActive = ((jobState as JobsStateWithJobs)
.jobList
.firstWhere((el) =>
el is ServiceToggleJob &&
el.type == serviceType) as ServiceToggleJob)
.needToTurnOn;
} else {
isActive = serviceState.isEnableByType(serviceType);
}
return BrandSwitch(
value: isActive,
onChanged: (value) =>
jobsCubit.createOrRemoveServiceToggleJob(
ServiceToggleJob(
type: serviceType,
needToTurnOn: value,
),
),
);
},
),
]
],
),
SizedBox(height: 10),
BrandText.h2(title),
SizedBox(height: 10),
BrandText.body2(subtitle),
SizedBox(height: 10),
ClipRect(
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 10),
BrandText.h2(serviceType.title),
SizedBox(height: 10),
BrandText.body2(serviceType.subtitle),
SizedBox(height: 10),
],
),
if (hasSwitchJob)
Positioned(
bottom: 24,
left: 0,
right: 0,
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 3,
sigmaY: 2,
),
child: BrandText.h2(
'jobs.runJobs'.tr(),
textAlign: TextAlign.center,
),
),
)
],
),
)
],
),
),
@ -139,16 +193,6 @@ class _Card extends StatelessWidget {
}
}
enum ServiceTypes {
mail,
messenger,
passwordManager,
video,
cloud,
socialNetwork,
git,
}
class _ServiceDetails extends StatelessWidget {
const _ServiceDetails({
Key? key,
@ -347,7 +391,12 @@ class _ServiceDetails extends StatelessWidget {
],
));
break;
case ServiceTypes.vpn:
child = Text(
'services.vpn.bottom_sheet.1'.tr(),
);
}
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
@ -359,7 +408,7 @@ class _ServiceDetails extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: brandPagePadding1,
padding: paddingH15V30,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -376,7 +425,7 @@ class _ServiceDetails extends StatelessWidget {
child: Container(
child: BrandButton.rised(
onPressed: () => Navigator.of(context).pop(),
title: 'basis.close'.tr(),
text: 'basis.close'.tr(),
),
),
),
@ -407,252 +456,3 @@ class _ServiceDetails extends StatelessWidget {
}
}
}
// class _ServiceDetails extends StatelessWidget {
// const _ServiceDetails({
// Key? key,
// required this.serviceType,
// required this.icon,
// required this.status,
// required this.title,
// required this.changeTab,
// }) : super(key: key);
// final ServiceTypes serviceType;
// final IconData icon;
// final StateType status;
// final String title;
// final ValueChanged<int> changeTab;
// @override
// Widget build(BuildContext context) {
// late Widget child;
// var config = context.watch<AppConfigCubit>().state;
// var domainName = UiHelpers.getDomainName(config);
// var linksStyle = body1Style.copyWith(
// fontSize: 15,
// color: Theme.of(context).brightness == Brightness.dark
// ? Colors.white
// : BrandColors.black,
// fontWeight: FontWeight.bold,
// decoration: TextDecoration.underline,
// // height: 1.1,
// );
// var textStyle = body1Style.copyWith(
// color: Theme.of(context).brightness == Brightness.dark
// ? Colors.white
// : BrandColors.black,
// );
// switch (serviceType) {
// case ServiceTypes.mail:
// child = RichText(
// text: TextSpan(
// children: [
// TextSpan(
// text: 'services.mail.bottom_sheet.1'.tr(args: [domainName]),
// style: textStyle,
// ),
// WidgetSpan(
// child: Padding(
// padding: EdgeInsets.only(bottom: 0.8, left: 5),
// child: GestureDetector(
// child: Text(
// 'services.mail.bottom_sheet.2'.tr(),
// style: linksStyle,
// ),
// onTap: () {
// Navigator.of(context).pop();
// changeTab(2);
// },
// ),
// ),
// ),
// ],
// ));
// break;
// case ServiceTypes.messenger:
// child = RichText(
// text: TextSpan(
// children: [
// TextSpan(
// text: 'services.messenger.bottom_sheet.1'.tr(args: [domainName]),
// style: textStyle,
// )
// ],
// ));
// break;
// case ServiceTypes.passwordManager:
// child = RichText(
// text: TextSpan(
// children: [
// TextSpan(
// text: 'services.password_manager.bottom_sheet.1'
// .tr(args: [domainName]),
// style: textStyle,
// ),
// WidgetSpan(
// child: Padding(
// padding: EdgeInsets.only(bottom: 0.8, left: 5),
// child: GestureDetector(
// onTap: () => _launchURL('https://password.$domainName'),
// child: Text(
// 'password.$domainName',
// style: linksStyle,
// ),
// ),
// ),
// ),
// ],
// ));
// break;
// case ServiceTypes.video:
// child = RichText(
// text: TextSpan(
// children: [
// TextSpan(
// text: 'services.video.bottom_sheet.1'.tr(args: [domainName]),
// style: textStyle,
// ),
// WidgetSpan(
// child: Padding(
// padding: EdgeInsets.only(bottom: 0.8, left: 5),
// child: GestureDetector(
// onTap: () => _launchURL('https://meet.$domainName'),
// child: Text(
// 'meet.$domainName',
// style: linksStyle,
// ),
// ),
// ),
// ),
// ],
// ));
// break;
// case ServiceTypes.cloud:
// child = RichText(
// text: TextSpan(
// children: [
// TextSpan(
// text: 'services.cloud.bottom_sheet.1'.tr(args: [domainName]),
// style: textStyle,
// ),
// WidgetSpan(
// child: Padding(
// padding: EdgeInsets.only(bottom: 0.8, left: 5),
// child: GestureDetector(
// onTap: () => _launchURL('https://cloud.$domainName'),
// child: Text(
// 'cloud.$domainName',
// style: linksStyle,
// ),
// ),
// ),
// ),
// ],
// ));
// break;
// case ServiceTypes.socialNetwork:
// child = RichText(
// text: TextSpan(
// children: [
// TextSpan(
// text: 'services.social_network.bottom_sheet.1'
// .tr(args: [domainName]),
// style: textStyle,
// ),
// WidgetSpan(
// child: Padding(
// padding: EdgeInsets.only(bottom: 0.8, left: 5),
// child: GestureDetector(
// onTap: () => _launchURL('https://social.$domainName'),
// child: Text(
// 'social.$domainName',
// style: linksStyle,
// ),
// ),
// ),
// ),
// ],
// ));
// break;
// case ServiceTypes.git:
// child = RichText(
// text: TextSpan(
// children: [
// TextSpan(
// text: 'services.git.bottom_sheet.1'.tr(args: [domainName]),
// style: textStyle,
// ),
// WidgetSpan(
// child: Padding(
// padding: EdgeInsets.only(bottom: 0.8, left: 5),
// child: GestureDetector(
// onTap: () => _launchURL('https://git.$domainName'),
// child: Text(
// 'git.$domainName',
// style: linksStyle,
// ),
// ),
// ),
// ),
// ],
// ));
// break;
// }
// return BrandModalSheet(
// child: Navigator(
// key: navigatorKey,
// initialRoute: '/',
// onGenerateRoute: (_) {
// return materialRoute(
// Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Padding(
// padding: brandPagePadding1,
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// SizedBox(height: 13),
// IconStatusMask(
// status: status,
// child: Icon(icon, size: 40, color: Colors.white),
// ),
// SizedBox(height: 10),
// BrandText.h1(title),
// SizedBox(height: 10),
// child,
// ],
// ),
// )
// ],
// ),
// );
// },
// ),
// );
// }
// void _launchURL(url) async {
// var _possible = await canLaunch(url);
// if (_possible) {
// try {
// await launch(
// url,
// forceSafariVC: true,
// enableJavaScript: true,
// );
// } catch (e) {
// print(e);
// }
// } else {
// throw 'Could not launch $url';
// }
// }
// }

View File

@ -24,7 +24,9 @@ class _Fab extends StatelessWidget {
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return _NewUser();
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: _NewUser());
},
);
},

View File

@ -7,10 +7,26 @@ class _NewUser extends StatelessWidget {
var domainName = UiHelpers.getDomainName(config);
return BrandModalSheet(
return BrandBottomSheet(
child: BlocProvider(
create: (context) =>
UserFormCubit(usersCubit: context.read<UsersCubit>()),
create: (context) {
var jobCubit = context.read<JobsCubit>();
var jobState = jobCubit.state;
var users = <User>[];
users.addAll(context.read<UsersCubit>().state.users);
if (jobState is JobsStateWithJobs) {
var jobs = jobState.jobList;
jobs.forEach((job) {
if (job is CreateUserJob) {
users.add(job.user);
}
});
}
return UserFormCubit(
jobsCubit: jobCubit,
users: users,
);
},
child: Builder(builder: (context) {
var formCubitState = context.watch<UserFormCubit>().state;
@ -22,20 +38,24 @@ class _NewUser extends StatelessWidget {
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
BrandHeader(
title: 'users.new_user'.tr(),
),
SizedBox(width: 14),
Padding(
padding: brandPagePadding2,
padding: paddingH15V0,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CubitFormTextField(
formFieldCubit: context.read<UserFormCubit>().login,
decoration: InputDecoration(
labelText: 'users.login'.tr(),
suffixText: '@$domainName',
IntrinsicHeight(
child: CubitFormTextField(
formFieldCubit: context.read<UserFormCubit>().login,
decoration: InputDecoration(
labelText: 'users.login'.tr(),
suffixText: '@$domainName',
),
),
),
SizedBox(height: 20),
@ -62,7 +82,7 @@ class _NewUser extends StatelessWidget {
onPressed: formCubitState.isSubmitting
? null
: () => context.read<UserFormCubit>().trySubmit(),
title: 'basis.create'.tr(),
text: 'basis.create'.tr(),
),
SizedBox(height: 40),
Text('users.new_user_info_note'.tr()),

View File

@ -1,24 +1,24 @@
part of 'users.dart';
class _User extends StatelessWidget {
const _User({Key? key, this.user}) : super(key: key);
const _User({Key? key, required this.user, required this.isRootUser})
: super(key: key);
final User? user;
final User user;
final bool isRootUser;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
showModalBottomSheet<void>(
showBrandBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return _UserDetails(user: user);
return _UserDetails(user: user, isRootUser: isRootUser);
},
);
},
child: Container(
padding: brandPagePadding2,
padding: paddingH15V0,
height: 48,
child: Row(
children: [
@ -26,12 +26,16 @@ class _User extends StatelessWidget {
width: 17,
height: 17,
decoration: BoxDecoration(
color: user!.color,
color: user.color,
shape: BoxShape.circle,
),
),
SizedBox(width: 20),
BrandText.h4(user!.login),
Flexible(
child: isRootUser
? BrandText.h4Underlined(user.login)
: BrandText.h4(user.login),
),
],
),
),

Some files were not shown because too many files have changed in this diff Show More