Compare commits

...

60 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
Illia Chub 5d8b953e0a Incremented application version 2021-04-28 20:00:41 +03:00
ilchub c3eed056b5 Add changelog for the updated version
Reviewed-on: kherel/selfprivacy.org.app#23
2021-04-27 18:30:33 +03:00
Illia Chub eaa059f964 Add changelog for the updated version 2021-04-27 18:28:57 +03:00
ilchub f30b54f564 Added charts. Added possibility to remove server. Fixed translations. Fixed black theme
Reviewed-on: kherel/selfprivacy.org.app#22
2021-04-27 12:58:59 +03:00
ilchub 739ff1aadf Added on-demand server removal option
Reviewed-on: kherel/selfprivacy.org.app#21
2021-04-22 21:22:38 +03:00
Kherel 8ccb4f18f6 done 2021-04-22 20:04:24 +02:00
Kherel 5b83b493f4 fix 2021-04-20 13:44:33 +02:00
Kherel 536ef4b717 add salt to hash 2021-04-19 14:37:51 +02:00
ilchub 695c91cba7 Changed infect script source from 'preproduction' to 'master' 2021-04-16 02:22:56 +03:00
Kherel 3676dc50f0 fix black theme 2021-04-10 12:06:29 +02:00
Kherel 6726c87de7 fix 2021-04-10 11:51:36 +02:00
Kherel cd49f9fb45 add charts 2021-04-10 05:04:23 +02:00
Illia Chub 039449d3f5 Updated getting started manual link 2021-04-08 14:43:48 +03:00
Illia Chub 160bdd51a3 Incremented version number back 2021-04-08 11:46:22 +03:00
120 changed files with 4131 additions and 2006 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 # Miscellaneous
*.class *.class
*.log *.log
@ -29,7 +30,7 @@
.packages .packages
.pub-cache/ .pub-cache/
.pub/ .pub/
/build/ **/build/
# Web related # Web related
lib/generated_plugin_registrant.dart lib/generated_plugin_registrant.dart

View File

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

View File

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

View File

@ -19,12 +19,15 @@
"connect": "Connect", "connect": "Connect",
"domain": "Domain", "domain": "Domain",
"saving": "Saving..", "saving": "Saving..",
"nickname": "nickname", "nickname": "Nickname",
"loading": "Loading...", "loading": "Loading...",
"later": "I will setup it later", "later": "I will setup it later",
"reset": "Reset", "reset": "Reset",
"details": "Details", "details": "Details",
"no_data": "No data" "no_data": "No data",
"wait": "Wait",
"remove": "Remove",
"apply": "Apply"
}, },
"more": { "more": {
"_comment": "'More' tab", "_comment": "'More' tab",
@ -32,16 +35,31 @@
"about_project": "About us", "about_project": "About us",
"about_app": "About application", "about_app": "About application",
"onboarding": "Onboarding", "onboarding": "Onboarding",
"create_ssh_key": "Create ssh key",
"generate_key": "Generate key",
"generate_key_text": "You can generate ssh key",
"console": "Console", "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": { "about_app_page": {
"text": "Тут любая служебная информация, v.{}" "text": "Application version v.{}"
}, },
"settings": { "settings": {
"title": "Application settings", "title": "Application settings",
"1": "Dark Theme", "1": "Dark Theme",
"2": "Change your the app theme", "2": "Change your the app theme",
"3": "Reset app config", "3": "Reset app config",
"4": "Reset api keys and root user" "4": "Reset api keys and root user",
"5": "Delete Server",
"6": "This removes the Server. It will be no longer accessible"
} }
}, },
"onboarding": { "onboarding": {
@ -61,14 +79,18 @@
"1": "It's a virtual computer, where all your services live.", "1": "It's a virtual computer, where all your services live.",
"2": "General information", "2": "General information",
"3": "Location" "3": "Location"
},
"chart": {
"month": "Month",
"day": "Day",
"hour": "Hour"
} }
}, },
"domain": { "domain": {
"card_title": "Domain", "card_title": "Domain",
"status": "Status — Good", "status": "Status — Good",
"bottom_sheet": { "bottom_sheet": {
"1": "It's your personal internet address that will point to the server and other services of yours.", "1": "It's your personal internet address that will point to the server and other services of yours."
"2": "{} — expires on {}"
} }
}, },
"backup": { "backup": {
@ -138,6 +160,13 @@
"bottom_sheet": { "bottom_sheet": {
"1": "You can connect and create a new user here:" "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": { "users": {
@ -157,7 +186,7 @@
}, },
"initializing": { "initializing": {
"_comment": "initializing page", "_comment": "initializing page",
"1": "Connect Hetzner server", "1": "Connect a server",
"2": "Here, your data and SelfPrivacy services wiil reside", "2": "Here, your data and SelfPrivacy services wiil reside",
"how": "How to obtain API token", "how": "How to obtain API token",
"3": "Connect CloudFlare", "3": "Connect CloudFlare",
@ -179,7 +208,11 @@
"18": "How to obtain Hetzner API Token", "18": "How to obtain Hetzner API Token",
"19": "1 Go via this link ", "19": "1 Go via this link ",
"20": "\n", "20": "\n",
"21": "One more restart to apply your security certificates." "21": "One more restart to apply your security certificates.",
"22": "Create master account",
"23": "Enter a nickname and strong password",
"finish": "Everything is initialized",
"checks": "Checks have been completed \n{} ouf of {}"
}, },
"modals": { "modals": {
"_comment": "messages in modals", "_comment": "messages in modals",
@ -187,9 +220,31 @@
"2": "Destroy server and create a new one?", "2": "Destroy server and create a new one?",
"3": "Are you sure?", "3": "Are you sure?",
"4": "Purge all authentication keys?", "4": "Purge all authentication keys?",
"5": "Yes, purge all my tokens" "5": "Yes, purge all my tokens",
"6": "Delete the server and volume?",
"7": "Yes",
"8": "Remove task"
}, },
"timer": { "timer": {
"sec": "{} sec" "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": "Провайдеры", "providers": "Провайдеры",
"services": "Сервисы", "services": "Сервисы",
"users": "Пользователи", "users": "Пользователи",
"more": "Еще", "more": "Ещё",
"next": "Далее", "next": "Далее",
"got_it": "Понял", "got_it": "Понял",
"settings": "Настройки", "settings": "Настройки",
@ -22,34 +22,52 @@
"nickname": "Никнейм", "nickname": "Никнейм",
"loading": "Загрузка", "loading": "Загрузка",
"later": "Настрою потом", "later": "Настрою потом",
"reset": "Reset", "reset": "Сбросить",
"details": "Детальная информация", "details": "Детальная информация",
"no_data": "Нет данных" "no_data": "Нет данных",
"wait": "Загрузка",
"remove": "Удалить",
"apply": "Подать"
}, },
"more": { "more": {
"_comment": "вкладка еще", "_comment": "вкладка ещё",
"configuration_wizard": "Мастер Подключения", "configuration_wizard": "Мастер Подключения",
"about_project": "О проекте SelfPrivacy", "about_project": "О проекте SelfPrivacy",
"about_app": "О приложении", "about_app": "О приложении",
"onboarding": "Приветствие", "onboarding": "Приветствие",
"console": "Консоль", "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": { "about_app_page": {
"text": "Версия приложения: v.{}" "text": "Версия приложения: v.{}"
}, },
"settings": { "settings": {
"title": "Настройки приложения", "title": "Настройки приложения",
"1": "Темная тема", "1": "Тёмная тема",
"2": "Сменить цветовую тему", "2": "Сменить цветовую тему.",
"3": "Сброс настроек", "3": "Сброс настроек",
"4": "Сбросить API ключи а так же root пользвателя" "4": "Сбросить API ключи а также root пользвателя.",
"5": "Удалить сервер",
"6": "Действие приведет к удалению сервера. После чего он не будет доступен."
} }
}, },
"onboarding": { "onboarding": {
"_comment": "страницы онбординга", "_comment": "страницы онбординга",
"page1_title": "Цифровая независимость доступна каждому", "page1_title": "Цифровая независимость доступна каждому",
"page1_text": "Почта, VPN, Мессенджер, социальная сеть и многое другое на вашем личном сервере, под вашим полным контролем.", "page1_text": "Почта, VPN, Мессенджер, социальная сеть и многое другое на Вашем личном сервере, под Вашим полным контролем.",
"page2_title": "SelfPrivacy — это не облако, а ваш личный дата-центр", "page2_title": "SelfPrivacy — это не облако, а Ваш личный дата-центр",
"page2_text": "У SelfPrivacy работает только с вашими сервис-провадерами: Hetzner, Cloudflare, Backblaze. Если у вас нет учетных записей, мы поможем их создать." "page2_text": "SelfPrivacy работает только с вашими сервис-провайдерами: Hetzner, Cloudflare, Backblaze. Если у Вас нет учётных записей, мы поможем их создать."
}, },
"providers": { "providers": {
"_comment": "вкладка провайдеры", "_comment": "вкладка провайдеры",
@ -58,25 +76,29 @@
"card_title": "Сервер", "card_title": "Сервер",
"status": "Статус — в норме", "status": "Статус — в норме",
"bottom_sheet": { "bottom_sheet": {
"1": "Это виртульный компьютер на котором работают все ваши сервисы.", "1": "Это виртуальный компьютер на котором работают все Ваши сервисы.",
"2": "Общая информация", "2": "Общая информация",
"3": "Размещение" "3": "Размещение"
},
"chart": {
"month": "Месяц",
"day": "День",
"hour": "Час"
} }
}, },
"domain": { "domain": {
"card_title": "Домен", "card_title": "Домен",
"status": "Статус — в норме", "status": "Статус — в норме",
"bottom_sheet": { "bottom_sheet": {
"1": "Это ваш личный адрес в интернете, который будет указывать на сервер и другие ваши сервисы.", "1": "Это ваш личный адрес в интернете, который будет указывать на сервер и другие ваши сервисы."
"2": "{} — продлен до {}"
} }
}, },
"backup": { "backup": {
"card_title": "Резервное копирование", "card_title": "Резервное копирование",
"status": "Статус — в норме", "status": "Статус — в норме",
"bottom_sheet": { "bottom_sheet": {
"1": "Выручит в любой ситуации: хакерская атака, удаление сервера и т.п.", "1": "Выручит Вас в любой ситуации: хакерская атака, удаление сервера и.т.д.",
"2": "3Gb — бестплатно до 10Gb, последний вчера в {}" "2": "Использовано 3Gb из бестплатых 10Gb. Последнее копирование была сделана вчера в {}."
} }
} }
}, },
@ -88,108 +110,141 @@
}, },
"services": { "services": {
"_comment": "Вкладка сервисы", "_comment": "Вкладка сервисы",
"title": "Ваши личные приватные и независимые сервисы", "title": "Ваши личные, приватные и независимые сервисы:",
"mail": { "mail": {
"title": "Почта", "title": "Почта",
"subtitle": "Электронная почта для семьи или компании", "subtitle": "Электронная почта для семьи или компании.",
"bottom_sheet": { "bottom_sheet": {
"1": "Для подключения почтового ящика используйте {} и логин пароль, который вы создали. Так же приглашайте", "1": "Для подключения почтового ящика используйте {} и профиль, который Вы создали. Так же приглашайте",
"2": "новых пользователей" "2": "новых пользователей."
} }
}, },
"messenger": { "messenger": {
"title": "Мессенджер", "title": "Мессенджер",
"subtitle": "Telegram и Signal не так приватны, как Delta.Chat использующий ваш личный сервер.", "subtitle": "Telegram и Signal не так приватны, как Delta.Chat — который использует Ваш личный сервер.",
"bottom_sheet": { "bottom_sheet": {
"1": "Для подключения используйте {} и логин пароль, который вы создали" "1": "Для подключения используйте {} и логин пароль, который Вы создали."
} }
}, },
"password_manager": { "password_manager": {
"title": "Менеджер паролей", "title": "Менеджер паролей",
"subtitle": "Фундамент безопасности. Создавать, хранить, копировать пароли между устройствами, вбивать их в формы поможет — Bitwarden.", "subtitle": "Это фундамент Вашей безопасности. Создавать, хранить, копировать пароли между устройствами и вбивать их в формы поможет — Bitwarden.",
"bottom_sheet": { "bottom_sheet": {
"1": "Подключиться к серверу и создать пользователя можно по адресу:" "1": "Подключиться к серверу и создать пользователя можно по адресу:."
} }
}, },
"video": { "video": {
"title": "Видеоконференция", "title": "Видеоконференция",
"subtitle": "Zoom и Google meet отличные инструменты, но Jitsi meet не хуже и дает уверенность, что вас никто не подслушивает.", "subtitle": "Jitsi meet — отличный аналог Zoom и Google meet который по мимо удобства ещё и гарантирует Вам защищённые высококачественные видеоконференции.",
"bottom_sheet": { "bottom_sheet": {
"1": "Для использования просто перейдите по ссылке:" "1": "Для использования просто перейдите по ссылке:."
} }
}, },
"cloud": { "cloud": {
"title": "Файловое облако", "title": "Файловое облако",
"subtitle": "Не позволяйте облачным сервисам читать ваши данные используйте NextCloud.", "subtitle": "Не позволяйте облачным сервисам просматривать ваши данные. Используйте NextCloud — надёжный дом для всех Ваших данных.",
"bottom_sheet": { "bottom_sheet": {
"1": "Подключиться к серверу и создать пользователя можно по адресу:" "1": "Подключиться к серверу и создать пользователя можно по адресу:."
} }
}, },
"social_network": { "social_network": {
"title": "Социальная сеть", "title": "Социальная сеть",
"subtitle": "Сложно поверить, но стало возможным создать свою собственную социальную сеть, со своими правилами и аудиторией.", "subtitle": "Сложно поверить, но стало возможным создать свою собственную социальную сеть, со своими правилами и аудиторией.",
"bottom_sheet": { "bottom_sheet": {
"1": "Подключиться к серверу и создать пользователя можно по адресу:" "1": "Подключиться к серверу и создать пользователя можно по адресу:."
} }
}, },
"git": { "git": {
"title": "Git-сервер", "title": "Git-сервер",
"subtitle": "Приватная альтернатива Github, которая принадлежит вам, а не Microsoft.", "subtitle": "Приватная альтернатива Github, которая принадлежит вам, а не Microsoft.",
"bottom_sheet": { "bottom_sheet": {
"1": "Подключиться к серверу и создать пользователя можно по адресу:" "1": "Подключиться к серверу и создать пользователя можно по адресу:."
}
},
"vpn": {
"title": "VPN сервер",
"subtitle": "Закрытый VPN сервер",
"bottom_sheet": {
"1": "Создать подключиться к VPN-серверу. Движок для безопасной и масштабируемой инфраструктуры VPN"
} }
} }
}, },
"users": { "users": {
"_comment": "'Users' tab", "_comment": "'Users' tab",
"add_new_user": "Добавьте первого пользователя", "add_new_user": "Добавьте первого пользователя.",
"new_user": "Новый пользователь", "new_user": "Новый пользователь",
"not_ready": "Подключите сервер, домен и DNS в разделу Провайдеры, чтобы добавить первого пользователя", "not_ready": "Подключите сервер, домен и DNS в разделу Провайдеры, чтобы добавить первого пользователя",
"nobody_here": "'Здесь пока никого'", "nobody_here": "Здесь будут отображаться пользователи.",
"login": "Логин", "login": "Логин",
"onboarding": "Приветствие", "onboarding": "Приветствие",
"console": "Консоль разработчика", "console": "Консоль разработчика",
"new_user_info_note": "Новый пользователь автоматически получит доступ ко всем сервисам. Ещё какое-то описание.", "new_user_info_note": "Новый пользователь автоматически получит доступ ко всем сервисам.",
"delete_confirm_question": "удалить учетную запись?", "delete_confirm_question": "Вы действительно хотите удалить учетную запись?",
"reset_password": "Сбросить пароль", "reset_password": "Сбросить пароль",
"account": "Учетная запись", "account": "Учетная запись",
"send_regisration_data": "Отправить реквизиты для входа" "send_regisration_data": "Поделиться реквизитами"
}, },
"initializing": { "initializing": {
"_comment": "initializing page", "_comment": "initializing page",
"1": "Подключите сервер Hetzner", "1": "Подключите сервер",
"2": "Здесь будут жить наши данные и SelfPrivacy-сервисы", "2": "Здесь будут жить наши данные и SelfPrivacy-сервисы",
"how": "Как получить API Token", "how": "Как получить API Token",
"3": "'Подключите CloudFlare'", "3": "Подключите CloudFlare",
"4": "Для управления DNS вашего домена", "4": "Для управления DNS вашего домена",
"5": "CloudFlare API Token", "5": "CloudFlare API Token",
"6": "Подключите облачное хранилище Backblaze", "6": "Подключите облачное хранилище Backblaze",
"7": "На данный момент подлюченных доменов нет", "7": "На данный момент подлюченных доменов нет",
"8": "Загружаем список доменов", "8": "Загружаем список доменов",
"9": "Найдено больше одного домена, для вашей безопастности, просим вам удалить не нужные домены", "9": "Найдено больше одного домена, для вашей безопастности, просим Вам удалить не нужные домены",
"10": "Сохранить домен", "10": "Сохранить домен",
"final": "Последний шаг", "final": "Последний шаг",
"11": "Создать сервер", "11": "Создать сервер",
"what": "Что это значит?", "what": "Что это значит?",
"13": "Сервер презагружен, ждем последнюю проверку", "13": "Сервер презагружен, ждем последнюю проверку.",
"14": "Cервер запушен, сейчас он будет проверен и перезагружен", "14": "Cервер запущен, сейчас он будет проверен и перезагружен.",
"15": "Cервер создан, идет проверка ДНС адресов и запуск сервера", "15": "Cервер создан, идет проверка ДНС адресов и запуск сервера.",
"16": "До следующей проверки: ", "16": "До следующей проверки: ",
"17": "Проверка", "17": "Проверка",
"18": "Как получить Hetzner API Token'", "18": "Как получить Hetzner API Token:'",
"19": "1 Переходим по ссылке ", "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": "Сейчас будет дополнительная перезагрузка для активации сертификатов безопастности" "21": "Сейчас будет дополнительная перезагрузка для активации сертификатов безопастности",
"22": "Создайте главную учетную запись",
"23": "Введите никнейм и сложный пароль",
"finish": "Всё инициализировано.",
"checks": "Проверок выполнено: \n{} / {}"
}, },
"modals": { "modals": {
"_comment": "messages in modals", "_comment": "messages in modals",
"1": "Сервер с таким именем уже существует", "1": "Сервер с таким именем уже существует",
"2": "Уничтожить сервер и создать новый?", "2": "Уничтожить сервер и создать новый?",
"3": "Вы уверенны", "3": "Подтвердите",
"4": "Сбросить все ключи?", "4": "Сбросить все ключи?",
"5": "Да, сбросить" "5": "Да, сбросить",
"6": "Удалить сервер и диск?",
"7": "Да, удалить",
"8": "Удалить задачу"
}, },
"timer": { "timer": {
"sec": "{} сек" "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,6 @@
- Updated Pleroma OTP to v2.3.0
- Updated Nextcloud to v21
- Fixed Bad Gateway HTTP response on social.$DOMAIN
- Fixed broken initial reboot chain
- Fixed password generation for PAM
- Various translation fixes

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

@ -1,5 +1,5 @@
SelfPrivacy - is a platform on your cloud hosting, that allows to deploy your own private services and control them using mobile application. SelfPrivacy - is a platform on your cloud hosting, that allows to deploy your own private services and control them using mobile application.
To use this application, you'll be required to create accounts of different service providers. Please reffer to this manual: https://hugo.selfprivacy.org/posts/getting_started To use this application, you'll be required to create accounts of different service providers. Please reffer to this manual: https://selfprivacy.org/en/second.html
Application will do the following things for you: Application will do the following things for you:
1. Create your personal server 1. Create your personal server
2. Setup NixOS 2. Setup NixOS
@ -12,5 +12,4 @@ Application will do the following things for you:
* Gitea - your own Git server * Gitea - your own Git server
* OpenConnect - Personal VPN 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. !!! 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.
!!! Only Russian localization is currently available. English is coming soon

View File

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

View File

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

View File

@ -345,7 +345,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@ -360,7 +360,8 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_TEAM = UVNTKR53DD;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -379,6 +380,7 @@
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
name = Profile; name = Profile;
@ -431,7 +433,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -481,7 +483,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@ -498,7 +500,8 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_TEAM = UVNTKR53DD;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -518,6 +521,7 @@
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
name = Debug; name = Debug;
@ -528,7 +532,8 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_TEAM = UVNTKR53DD;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -547,6 +552,7 @@
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
name = Release; 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"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleLocalizations</key>
<array>
<string>ru</string>
<string>en</string>
</array>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
@ -15,6 +10,11 @@
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>ru</string>
<string>en</string>
</array>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>selfprivacy</string> <string>selfprivacy</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
@ -24,7 +24,7 @@
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
@ -34,8 +34,6 @@
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <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:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.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/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/providers/providers_cubit.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
class BlocAndProviderConfig extends StatelessWidget { class BlocAndProviderConfig extends StatelessWidget {
@ -12,11 +14,10 @@ class BlocAndProviderConfig extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// var platformBrightness =
// SchedulerBinding.instance.window.platformBrightness;
// var isDark = platformBrightness == Brightness.dark;
var isDark = false; var isDark = false;
var usersCubit = UsersCubit();
var appConfigCubit = AppConfigCubit()..load();
var servicesCubit = ServicesCubit(appConfigCubit);
return MultiProvider( return MultiProvider(
providers: [ providers: [
BlocProvider( BlocProvider(
@ -25,12 +26,14 @@ class BlocAndProviderConfig extends StatelessWidget {
isOnbordingShowing: true, isOnbordingShowing: true,
)..load(), )..load(),
), ),
BlocProvider( BlocProvider(lazy: false, create: (_) => appConfigCubit),
lazy: false,
create: (_) => AppConfigCubit()..load(),
),
BlocProvider(create: (_) => ProvidersCubit()), 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, 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/api_config.dart';
import 'package:selfprivacy/logic/get_it/console.dart'; import 'package:selfprivacy/logic/get_it/console.dart';
import 'package:selfprivacy/logic/get_it/navigation.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'; import 'package:selfprivacy/logic/get_it/timer.dart';
export 'package:selfprivacy/logic/get_it/api_config.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/navigation.dart';
export 'package:selfprivacy/logic/get_it/timer.dart'; export 'package:selfprivacy/logic/get_it/timer.dart';
final getIt = GetIt.instance; final getIt = GetIt.instance;
Future<void> getItSetup() async { Future<void> getItSetup() async {
@ -17,6 +17,7 @@ Future<void> getItSetup() async {
getIt.registerSingleton<ConsoleModel>(ConsoleModel()); getIt.registerSingleton<ConsoleModel>(ConsoleModel());
getIt.registerSingleton<TimerModel>(TimerModel()); getIt.registerSingleton<TimerModel>(TimerModel());
getIt.registerSingleton<SSHModel>(SSHModel()..init());
getIt.registerSingleton<ApiConfigModel>(ApiConfigModel()..init()); getIt.registerSingleton<ApiConfigModel>(ApiConfigModel()..init());
await getIt.allReady(); await getIt.allReady();

View File

@ -19,21 +19,25 @@ class HiveConfig {
Hive.registerAdapter(HetznerDataBaseAdapter()); Hive.registerAdapter(HetznerDataBaseAdapter());
await Hive.openBox(BNames.appSettings); 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); 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 { static Future<Uint8List> getEncriptedKey(String encKey) async {
final FlutterSecureStorage secureStorage = FlutterSecureStorage(); final secureStorage = FlutterSecureStorage();
var containsEncryptionKey = var hasEncryptionKey = await secureStorage.containsKey(key: encKey);
await secureStorage.containsKey(key: BNames.key); if (!hasEncryptionKey) {
if (!containsEncryptionKey) {
var key = Hive.generateSecureKey(); 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!); return base64Url.decode(string!);
} }
} }
@ -42,10 +46,13 @@ class BNames {
static String appConfig = 'appConfig'; static String appConfig = 'appConfig';
static String isDarkModeOn = 'isDarkModeOn'; static String isDarkModeOn = 'isDarkModeOn';
static String isOnbordingShowing = 'isOnbordingShowing'; static String isOnbordingShowing = 'isOnbordingShowing';
static String users = 'users';
static String appSettings = 'appSettings'; static String appSettings = 'appSettings';
static String servicesState = 'servicesState';
static String key = 'key'; static String key = 'key';
static String sshEnckey = 'sshEngkey';
static String cloudFlareDomain = 'cloudFlareDomain'; static String cloudFlareDomain = 'cloudFlareDomain';
static String hetznerKey = 'hetznerKey'; static String hetznerKey = 'hetznerKey';
@ -58,4 +65,7 @@ class BNames {
static String isLoading = 'isLoading'; static String isLoading = 'isLoading';
static String isServerResetedFirstTime = 'isServerResetedFirstTime'; static String isServerResetedFirstTime = 'isServerResetedFirstTime';
static String isServerResetedSecondTime = 'isServerResetedSecondTime'; 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, color: BrandColors.headlineColor,
); );
final headline4UnderlinedStyle = defaultTextStyle.copyWith(
fontSize: 18,
fontWeight: NamedFontWeight.medium,
color: BrandColors.headlineColor,
decoration: TextDecoration.underline,
);
final headline5Style = defaultTextStyle.copyWith( final headline5Style = defaultTextStyle.copyWith(
fontSize: 15, fontSize: 15,
fontWeight: NamedFontWeight.medium, fontWeight: NamedFontWeight.medium,

View File

@ -15,13 +15,23 @@ abstract class ApiMap {
if (hasLoger) { if (hasLoger) {
dio.interceptors.add(PrettyDioLogger()); dio.interceptors.add(PrettyDioLogger());
} }
dio..interceptors.add(ConsoleInterceptor()); dio.interceptors.add(ConsoleInterceptor());
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(HttpClient client) { (HttpClient client) {
client.badCertificateCallback = client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true; (X509Certificate cert, String host, int port) => true;
return client; 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; 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/hetzner_server_info.dart';
import 'package:selfprivacy/logic/models/server_details.dart'; import 'package:selfprivacy/logic/models/server_details.dart';
import 'package:selfprivacy/logic/models/user.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 { class HetznerApi extends ApiMap {
bool hasLoger; bool hasLoger;
@ -68,24 +68,13 @@ class HetznerApi extends ApiMap {
return server == null; return server == null;
} }
Future<HetznerServerDetails> createServer({ Future<HetznerDataBase> createVolume() async {
required String cloudFlareKey,
required User rootUser,
required String domainName,
}) async {
var dbPassword = getRandomString(40);
const chars =
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
var dbStorageName = getRandomString(6, chars);
var client = await getClient(); var client = await getClient();
Response dbCreateResponse = await client.post( Response dbCreateResponse = await client.post(
'/volumes', '/volumes',
data: { data: {
"size": 10, "size": 10,
"name": dbStorageName, "name": StringGenerators.dbStorageName(),
"labels": {"labelkey": "value"}, "labels": {"labelkey": "value"},
"location": "fsn1", "location": "fsn1",
"automount": false, "automount": false,
@ -93,23 +82,53 @@ class HetznerApi extends ApiMap {
}, },
); );
var dbId = dbCreateResponse.data['volume']['id']; var dbId = dbCreateResponse.data['volume']['id'];
var data = jsonDecode( return HetznerDataBase(
'''{"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/preproduction/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-20.09 DOMAIN=$domainName LUSER=${rootUser.login} PASSWORD=${rootUser.password} HASHED_PASSWORD=${rootUser.hashPassword} CF_TOKEN=$cloudFlareKey DB_PASSWORD=$dbPassword bash 2>&1 | tee /tmp/infect.log","labels":{},"automount":true, "location": "fsn1"}''', 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/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( Response serverCreateResponse = await client.post(
'/servers', '/servers',
data: data, data: data,
); );
client.close(); client.close();
return HetznerServerDetails( return HetznerServerDetails(
id: serverCreateResponse.data['server']['id'], id: serverCreateResponse.data['server']['id'],
ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'], ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'],
createTime: DateTime.now(), createTime: DateTime.now(),
dataBase: HetznerDataBase( dataBase: dataBase,
id: dbId,
name: dbCreateResponse.data['volume']['name'],
),
); );
} }
@ -120,28 +139,22 @@ class HetznerApi extends ApiMap {
Response serversReponse = await client.get('/servers'); Response serversReponse = await client.get('/servers');
List servers = serversReponse.data['servers']; List servers = serversReponse.data['servers'];
var server = servers.firstWhere((el) => el['name'] == domainName); Map server = servers.firstWhere((el) => el['name'] == domainName);
await client.delete('/servers/${server['id']}'); List volumes = server['volumes'];
Response volumesReponse = await client.get('/volumes');
List volumes = volumesReponse.data['volumes'];
var laterFutures = <Future>[]; 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) { for (var volumeId in volumes) {
close(client); await client.post('/volumes/$volumeId/actions/detach');
} else {
Future.wait(laterFutures).then((value) => close(client));
} }
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 { Future<HetznerServerDetails> reset() async {
@ -164,11 +177,22 @@ class HetznerApi extends ApiMap {
return server.copyWith(startTime: DateTime.now()); return server.copyWith(startTime: DateTime.now());
} }
metrics() async { Future<Map<String, dynamic>> getMetrics(
DateTime start, DateTime end, String type) async {
var hetznerServer = getIt<ApiConfigModel>().hetznerServer; var hetznerServer = getIt<ApiConfigModel>().hetznerServer;
var client = await getClient(); var client = await getClient();
await client.post('/servers/${hetznerServer!.id}/metrics');
Map<String, dynamic> queryParameters = {
"start": start.toUtc().toIso8601String(),
"end": end.toUtc().toIso8601String(),
"type": type
};
var res = await client.get(
'/servers/${hetznerServer!.id}/metrics',
queryParameters: queryParameters,
);
close(client); close(client);
return res.data;
} }
Future<HetznerServerInfo> getInfo() async { Future<HetznerServerInfo> getInfo() async {
@ -179,4 +203,20 @@ class HetznerApi extends ApiMap {
return HetznerServerInfo.fromJson(response.data!['server']); 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:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.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'; import 'api_map.dart';
@ -31,7 +33,7 @@ class ServerApi extends ApiMap {
var client = await getClient(); var client = await getClient();
try { try {
response = await client.get('/serviceStatus'); response = await client.get('/services/status');
res = response.statusCode == HttpStatus.ok; res = response.statusCode == HttpStatus.ok;
} catch (e) { } catch (e) {
res = false; res = false;
@ -40,6 +42,106 @@ class ServerApi extends ApiMap {
return res; 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 => String get rootAddress =>
throw UnimplementedError('not used in with implementation'); 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 { enum InitializingSteps {
setHeznerKey, setHeznerKey,
setCloudFlareKey, setCloudFlareKey,
@ -8,3 +13,82 @@ enum InitializingSteps {
startServer, startServer,
checkSystemDnsAndDkimSet, checkSystemDnsAndDkimSet,
} }
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:bloc/bloc.dart';
import 'package:equatable/equatable.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/backblaze_credential.dart';
import 'package:selfprivacy/logic/models/cloudflare_domain.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 /// c. if server is okay set that fully checked
class AppConfigCubit extends Cubit<AppConfigState> { class AppConfigCubit extends Cubit<AppConfigState> {
AppConfigCubit() : super(InitialAppConfigState()); AppConfigCubit() : super(AppConfigEmpty());
final repository = AppConfigRepository(); final repository = AppConfigRepository();
Future<void> load() async { Future<void> load() async {
var state = await repository.load(); var state = await repository.load();
if (state.progress < 6 || state.isFullyInitilized) { if (state is AppConfigFinished) {
emit(state); emit(state);
} else if (state.progress == 6) { } else if (state is AppConfigNotFinished) {
startServerIfDnsIsOkay(state: state, isImmediate: true); if (state.progress == 6) {
} else if (state.progress == 7) { startServerIfDnsIsOkay(state: state, isImmediate: true);
resetServerIfServerIsOkay(state: state, isImmediate: true); } else if (state.progress == 7) {
} else if (state.progress == 8) { resetServerIfServerIsOkay(state: state, isImmediate: true);
oneMoreReset(state: state, isImmediate: true); } else if (state.progress == 8) {
} else if (state.progress == 9) { oneMoreReset(state: state, isImmediate: true);
finishCheckIfServerIsOkay(state: state, isImmediate: true); } else if (state.progress == 9) {
finishCheckIfServerIsOkay(state: state, isImmediate: true);
}
} else {
throw 'wrong state';
} }
} }
void startServerIfDnsIsOkay({ void startServerIfDnsIsOkay({
AppConfigState? state, AppConfigNotFinished? state,
bool isImmediate = false, bool isImmediate = false,
}) async { }) async {
state = state ?? this.state; state = state ?? this.state as AppConfigNotFinished;
final work = () async { final work = () async {
emit(TimerState(dataState: state!, isLoading: true)); emit(TimerState(dataState: state!, isLoading: true));
@ -112,10 +118,10 @@ class AppConfigCubit extends Cubit<AppConfigState> {
} }
void oneMoreReset({ void oneMoreReset({
AppConfigState? state, AppConfigNotFinished? state,
bool isImmediate = false, bool isImmediate = false,
}) async { }) async {
var dataState = state ?? this.state; var dataState = state ?? this.state as AppConfigNotFinished;
var work = () async { var work = () async {
emit(TimerState(dataState: dataState, isLoading: true)); emit(TimerState(dataState: dataState, isLoading: true));
@ -165,10 +171,10 @@ class AppConfigCubit extends Cubit<AppConfigState> {
} }
void resetServerIfServerIsOkay({ void resetServerIfServerIsOkay({
AppConfigState? state, AppConfigNotFinished? state,
bool isImmediate = false, bool isImmediate = false,
}) async { }) async {
var dataState = state ?? this.state; var dataState = state ?? this.state as AppConfigNotFinished;
var work = () async { var work = () async {
emit(TimerState(dataState: dataState, isLoading: true)); emit(TimerState(dataState: dataState, isLoading: true));
@ -220,10 +226,10 @@ class AppConfigCubit extends Cubit<AppConfigState> {
Timer? timer; Timer? timer;
void finishCheckIfServerIsOkay({ void finishCheckIfServerIsOkay({
AppConfigState? state, AppConfigNotFinished? state,
bool isImmediate = false, bool isImmediate = false,
}) async { }) async {
state = state ?? this.state; state = state ?? this.state as AppConfigNotFinished;
var work = () async { var work = () async {
emit(TimerState(dataState: state!, isLoading: true)); emit(TimerState(dataState: state!, isLoading: true));
@ -233,10 +239,7 @@ class AppConfigCubit extends Cubit<AppConfigState> {
if (isServerWorking) { if (isServerWorking) {
await repository.saveHasFinalChecked(true); await repository.saveHasFinalChecked(true);
emit(state.copyWith( emit(state.finish());
hasFinalChecked: true,
isLoading: false,
));
} else { } else {
finishCheckIfServerIsOkay(); finishCheckIfServerIsOkay();
} }
@ -259,18 +262,42 @@ class AppConfigCubit extends Cubit<AppConfigState> {
void clearAppConfig() { void clearAppConfig() {
closeTimer(); closeTimer();
repository.clearAppConfig(); 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(AppConfigNotFinished(
hetznerKey: state.hetznerKey,
cloudFlareDomain: state.cloudFlareDomain,
cloudFlareKey: state.cloudFlareKey,
backblazeCredential: state.backblazeCredential,
rootUser: state.rootUser,
hetznerServer: null,
isServerStarted: false,
isServerResetedFirstTime: false,
isServerResetedSecondTime: false,
isLoading: false,
));
} }
void setHetznerKey(String hetznerKey) async { void setHetznerKey(String hetznerKey) async {
await repository.saveHetznerKey(hetznerKey); await repository.saveHetznerKey(hetznerKey);
emit(state.copyWith(hetznerKey: hetznerKey)); emit((state as AppConfigNotFinished).copyWith(hetznerKey: hetznerKey));
} }
void setCloudflareKey(String cloudFlareKey) async { void setCloudflareKey(String cloudFlareKey) async {
await repository.saveCloudFlareKey(cloudFlareKey); await repository.saveCloudFlareKey(cloudFlareKey);
emit(state.copyWith(cloudFlareKey: cloudFlareKey)); emit(
(state as AppConfigNotFinished).copyWith(cloudFlareKey: cloudFlareKey));
} }
void setBackblazeKey(String keyId, String applicationKey) async { void setBackblazeKey(String keyId, String applicationKey) async {
@ -279,38 +306,41 @@ class AppConfigCubit extends Cubit<AppConfigState> {
applicationKey: applicationKey, applicationKey: applicationKey,
); );
await repository.saveBackblazeKey(backblazeCredential); await repository.saveBackblazeKey(backblazeCredential);
emit(state.copyWith(backblazeCredential: backblazeCredential)); emit((state as AppConfigNotFinished)
.copyWith(backblazeCredential: backblazeCredential));
} }
void setDomain(CloudFlareDomain cloudFlareDomain) async { void setDomain(CloudFlareDomain cloudFlareDomain) async {
await repository.saveDomain(cloudFlareDomain); await repository.saveDomain(cloudFlareDomain);
emit(state.copyWith(cloudFlareDomain: cloudFlareDomain)); emit((state as AppConfigNotFinished)
.copyWith(cloudFlareDomain: cloudFlareDomain));
} }
void setRootUser(User rootUser) async { void setRootUser(User rootUser) async {
await repository.saveRootUser(rootUser); await repository.saveRootUser(rootUser);
emit(state.copyWith(rootUser: rootUser)); emit((state as AppConfigNotFinished).copyWith(rootUser: rootUser));
} }
void createServerAndSetDnsRecords() async { void createServerAndSetDnsRecords() async {
AppConfigState _stateCopy = state; AppConfigNotFinished _stateCopy = state as AppConfigNotFinished;
var onSuccess = (serverDetails) async { var onSuccess = (HetznerServerDetails serverDetails) async {
await repository.createDnsRecords( await repository.createDnsRecords(
serverDetails.ip4, serverDetails.ip4,
state.cloudFlareDomain!, state.cloudFlareDomain!,
); );
emit(state.copyWith( emit((state as AppConfigNotFinished).copyWith(
isLoading: false, isLoading: false,
hetznerServer: serverDetails, hetznerServer: serverDetails,
)); ));
startServerIfDnsIsOkay(); startServerIfDnsIsOkay();
}; };
var onCancel = () => emit(state.copyWith(isLoading: false)); var onCancel =
() => emit((state as AppConfigNotFinished).copyWith(isLoading: false));
try { try {
emit(state.copyWith(isLoading: true)); emit((state as AppConfigNotFinished).copyWith(isLoading: true));
await repository.createServer( await repository.createServer(
state.rootUser!, state.rootUser!,
state.cloudFlareDomain!.domainName, state.cloudFlareDomain!.domainName,

View File

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

View File

@ -1,6 +1,6 @@
part of 'app_config_cubit.dart'; part of 'app_config_cubit.dart';
class AppConfigState extends Equatable { abstract class AppConfigState extends Equatable {
const AppConfigState({ const AppConfigState({
required this.hetznerKey, required this.hetznerKey,
required this.cloudFlareKey, required this.cloudFlareKey,
@ -11,9 +11,6 @@ class AppConfigState extends Equatable {
required this.isServerStarted, required this.isServerStarted,
required this.isServerResetedFirstTime, required this.isServerResetedFirstTime,
required this.isServerResetedSecondTime, required this.isServerResetedSecondTime,
required this.hasFinalChecked,
required this.isLoading,
required this.error,
}); });
@override @override
@ -26,9 +23,6 @@ class AppConfigState extends Equatable {
hetznerServer, hetznerServer,
isServerStarted, isServerStarted,
isServerResetedFirstTime, isServerResetedFirstTime,
hasFinalChecked,
isLoading,
error,
]; ];
final String? hetznerKey; final String? hetznerKey;
@ -41,42 +35,6 @@ class AppConfigState extends Equatable {
final bool isServerResetedFirstTime; final bool isServerResetedFirstTime;
final bool isServerResetedSecondTime; 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 isHetznerFilled => hetznerKey != null;
bool get isCloudFlareFilled => cloudFlareKey != null; bool get isCloudFlareFilled => cloudFlareKey != null;
bool get isBackblazeFilled => backblazeCredential != null; bool get isBackblazeFilled => backblazeCredential != null;
@ -84,9 +42,19 @@ class AppConfigState extends Equatable {
bool get isUserFilled => rootUser != null; bool get isUserFilled => rootUser != null;
bool get isServerCreated => hetznerServer != 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 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 { List<bool?> get _fulfilementList {
var res = [ var res = [
isHetznerFilled, isHetznerFilled,
@ -98,32 +66,13 @@ class AppConfigState extends Equatable {
isServerStarted, isServerStarted,
isServerResetedFirstTime, isServerResetedFirstTime,
isServerResetedSecondTime, isServerResetedSecondTime,
hasFinalChecked,
]; ];
return res; return res;
} }
} }
class InitialAppConfigState extends AppConfigState { class TimerState extends AppConfigNotFinished {
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 {
TimerState({ TimerState({
required this.dataState, required this.dataState,
this.timerStart, this.timerStart,
@ -139,12 +88,10 @@ class TimerState extends AppConfigState {
isServerStarted: dataState.isServerStarted, isServerStarted: dataState.isServerStarted,
isServerResetedFirstTime: dataState.isServerResetedFirstTime, isServerResetedFirstTime: dataState.isServerResetedFirstTime,
isServerResetedSecondTime: dataState.isServerResetedSecondTime, isServerResetedSecondTime: dataState.isServerResetedSecondTime,
hasFinalChecked: dataState.hasFinalChecked,
isLoading: isLoading, isLoading: isLoading,
error: dataState.error,
); );
final AppConfigState dataState; final AppConfigNotFinished dataState;
final DateTime? timerStart; final DateTime? timerStart;
final Duration? duration; final Duration? duration;
@ -155,3 +102,134 @@ class TimerState extends AppConfigState {
duration, 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/api_maps/backblaze.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/models/backblaze_credential.dart'; import 'package:selfprivacy/logic/models/backblaze_credential.dart';
import 'package:easy_localization/easy_localization.dart';
class BackblazeFormCubit extends FormCubit { class BackblazeFormCubit extends FormCubit {
BackblazeFormCubit(this.initializingCubit) { BackblazeFormCubit(this.initializingCubit) {
@ -10,7 +11,7 @@ class BackblazeFormCubit extends FormCubit {
keyId = FieldCubit( keyId = FieldCubit(
initalValue: '', initalValue: '',
validations: [ validations: [
RequiredStringValidation('required'), RequiredStringValidation('validations.required'.tr()),
//ValidationModel<String>( //ValidationModel<String>(
//(s) => regExp.hasMatch(s), 'invalid key format'), //(s) => regExp.hasMatch(s), 'invalid key format'),
//LegnthStringValidationWithLenghShowing(64, 'length is [] shoud be 64') //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/api_maps/cloudflare.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart';
import 'package:easy_localization/easy_localization.dart';
class CloudFlareFormCubit extends FormCubit { class CloudFlareFormCubit extends FormCubit {
CloudFlareFormCubit(this.initializingCubit) { CloudFlareFormCubit(this.initializingCubit) {
@ -11,10 +12,11 @@ class CloudFlareFormCubit extends FormCubit {
apiKey = FieldCubit( apiKey = FieldCubit(
initalValue: '', initalValue: '',
validations: [ validations: [
RequiredStringValidation('required'), RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>( ValidationModel<String>(
(s) => regExp.hasMatch(s), 'invalid key format'), (s) => regExp.hasMatch(s), 'validations.key_format'.tr()),
LegnthStringValidationWithLenghShowing(40, 'length is [] shoud be 40') 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/api_maps/hetzner.dart';
import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:easy_localization/easy_localization.dart';
class HetznerFormCubit extends FormCubit { class HetznerFormCubit extends FormCubit {
HetznerFormCubit(this.initializingCubit) { HetznerFormCubit(this.initializingCubit) {
@ -11,10 +12,10 @@ class HetznerFormCubit extends FormCubit {
apiKey = FieldCubit( apiKey = FieldCubit(
initalValue: '', initalValue: '',
validations: [ validations: [
RequiredStringValidation('required'), RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>( ValidationModel<String>(
(s) => regExp.hasMatch(s), 'invalid key format'), (s) => regExp.hasMatch(s), 'validations.key_format'.tr()),
LegnthStringValidationWithLenghShowing(64, 'length is [] shoud be 64') 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:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/models/user.dart'; import 'package:selfprivacy/logic/models/user.dart';
import 'package:easy_localization/easy_localization.dart';
class RootUserFormCubit extends FormCubit { class RootUserFormCubit extends FormCubit {
RootUserFormCubit(this.initializingCubit) { RootUserFormCubit(this.initializingCubit) {
@ -12,18 +13,20 @@ class RootUserFormCubit extends FormCubit {
userName = FieldCubit( userName = FieldCubit(
initalValue: '', initalValue: '',
validations: [ validations: [
RequiredStringValidation('required'),
ValidationModel<String>( 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( password = FieldCubit(
initalValue: '', initalValue: '',
validations: [ validations: [
RequiredStringValidation('required'), RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>( 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 'dart:async';
import 'package:cubit_form/cubit_form.dart'; 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:selfprivacy/logic/models/user.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/utils/password_generator.dart'; import 'package:selfprivacy/utils/password_generator.dart';
class UserFormCubit extends FormCubit { class UserFormCubit extends FormCubit {
UserFormCubit({ UserFormCubit({
required this.usersCubit, required this.jobsCubit,
required List<User> users,
User? user, User? user,
}) { }) {
var isEdit = user != null; var isEdit = user != null;
@ -18,18 +21,22 @@ class UserFormCubit extends FormCubit {
login = FieldCubit( login = FieldCubit(
initalValue: isEdit ? user!.login : '', initalValue: isEdit ? user!.login : '',
validations: [ validations: [
RequiredStringValidation('required'), ValidationModel(
(login) => users.any((user) => user.login == login),
'validations.user_alredy_exist'.tr(),
),
RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>( ValidationModel<String>(
(s) => userRegExp.hasMatch(s), 'invalid format'), (s) => userRegExp.hasMatch(s), 'validations.invalid_format'.tr()),
], ],
); );
password = FieldCubit( password = FieldCubit(
initalValue: isEdit ? user!.password : genPass(), initalValue: isEdit ? user!.password : StringGenerators.userPassword(),
validations: [ validations: [
RequiredStringValidation('required'), RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>( ValidationModel<String>((s) => passwordRegExp.hasMatch(s),
(s) => passwordRegExp.hasMatch(s), 'invalid format'), 'validations.invalid_format'.tr()),
], ],
); );
@ -42,15 +49,15 @@ class UserFormCubit extends FormCubit {
login: login.state.value, login: login.state.value,
password: password.state.value, password: password.state.value,
); );
usersCubit.addUser(user); jobsCubit.addJob(CreateUserJob(user: user));
} }
late FieldCubit<String> login; late FieldCubit<String> login;
late FieldCubit<String> password; late FieldCubit<String> password;
void genNewPassword() { void genNewPassword() {
password.externalSetValue(genPass()); password.externalSetValue(StringGenerators.userPassword());
} }
late UsersCubit usersCubit; final JobsCubit jobsCubit;
} }

View File

@ -0,0 +1,49 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/models/hetzner_metrics.dart';
import 'hetzner_metrics_repository.dart';
part 'hetzner_metrics_state.dart';
class HetznerMetricsCubit extends Cubit<HetznerMetricsState> {
HetznerMetricsCubit() : super(HetznerMetricsLoading(Period.day));
final repository = HetznerMetricsRepository();
Timer? timer;
close() {
closeTimer();
return super.close();
}
void closeTimer() {
if (timer != null && timer!.isActive) {
timer!.cancel();
}
}
void changePeriod(Period period) async {
closeTimer();
emit(HetznerMetricsLoading(period));
load(period);
}
void restart() async {
load(state.period);
}
void load(Period period) async {
var newState = await repository.getMetrics(period);
timer = Timer(
Duration(seconds: newState.stepInSeconds.toInt()),
() => load(newState.period),
);
emit(newState);
}
}

View File

@ -0,0 +1,56 @@
import 'package:selfprivacy/logic/api_maps/hetzner.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/models/hetzner_metrics.dart';
import 'hetzner_metrics_cubit.dart';
class HetznerMetricsRepository {
Future<HetznerMetricsLoaded> getMetrics(Period period) async {
var end = DateTime.now();
DateTime start;
switch (period) {
case Period.hour:
start = end.subtract(Duration(hours: 1));
break;
case Period.day:
start = end.subtract(Duration(days: 1));
break;
case Period.month:
start = end.subtract(Duration(days: 15));
break;
}
var api = HetznerApi(hasLoger: true);
var results = await Future.wait([
api.getMetrics(start, end, 'cpu'),
api.getMetrics(start, end, 'network'),
]);
var cpuMetricsData = results[0]["metrics"];
var networkMetricsData = results[1]["metrics"];
return HetznerMetricsLoaded(
period: period,
start: start,
end: end,
stepInSeconds: cpuMetricsData["step"],
cpu: timeSeriesSerializer(cpuMetricsData, 'cpu'),
ppsIn: timeSeriesSerializer(networkMetricsData, 'network.0.pps.in'),
ppsOut: timeSeriesSerializer(networkMetricsData, 'network.0.pps.out'),
bandwidthIn:
timeSeriesSerializer(networkMetricsData, 'network.0.bandwidth.in'),
bandwidthOut: timeSeriesSerializer(
networkMetricsData,
'network.0.bandwidth.out',
),
);
}
}
List<TimeSeriesData> timeSeriesSerializer(
Map<String, dynamic> json, String type) {
List list = json["time_series"][type]["values"];
return list.map((el) => TimeSeriesData(el[0], double.parse(el[1]))).toList();
}

View File

@ -0,0 +1,43 @@
part of 'hetzner_metrics_cubit.dart';
abstract class HetznerMetricsState extends Equatable {
const HetznerMetricsState();
abstract final Period period;
}
class HetznerMetricsLoading extends HetznerMetricsState {
HetznerMetricsLoading(this.period);
final Period period;
@override
List<Object?> get props => [period];
}
class HetznerMetricsLoaded extends HetznerMetricsState {
HetznerMetricsLoaded({
required this.period,
required this.start,
required this.end,
required this.stepInSeconds,
required this.cpu,
required this.ppsIn,
required this.ppsOut,
required this.bandwidthIn,
required this.bandwidthOut,
});
final Period period;
final DateTime start;
final DateTime end;
final num stepInSeconds;
final List<TimeSeriesData> cpu;
final List<TimeSeriesData> ppsIn;
final List<TimeSeriesData> ppsOut;
final List<TimeSeriesData> bandwidthIn;
final List<TimeSeriesData> bandwidthOut;
@override
List<Object?> get props => [period, start, end];
}

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:bloc/bloc.dart';
import 'package:equatable/equatable.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'; import 'package:selfprivacy/logic/models/user.dart';
export 'package:provider/provider.dart'; export 'package:provider/provider.dart';
part 'users_state.dart'; part 'users_state.dart';
class UsersCubit extends Cubit<UsersState> { class UsersCubit extends Cubit<UsersState> {
UsersCubit() : super(UsersState([])); UsersCubit() : super(UsersState(<User>[]));
Box<User> box = Hive.box<User>(BNames.users);
void addUser(User user) { void load() async {
var users = [...state.users]; var loadedUsers = box.values.toList();
users.add(user);
if (loadedUsers.isNotEmpty) {
emit(UsersState(users)); 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 users = [...state.users];
var index = users.indexOf(user);
users.remove(user); users.remove(user);
await box.deleteAt(index);
emit(UsersState(users)); 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/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/text_themes.dart';
class NavigationService { class NavigationService {
final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
NavigatorState? get navigator => navigatorKey.currentState; NavigatorState? get navigator => navigatorKey.currentState;
void showPopUpDialog(AlertDialog dialog) { void showPopUpDialog(AlertDialog dialog) {
@ -13,4 +18,14 @@ class NavigationService {
builder: (_) => dialog, 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();
}
}

View File

@ -0,0 +1,11 @@
class TimeSeriesData {
TimeSeriesData(
this.secondsSinceEpoch,
this.value,
);
final int secondsSinceEpoch;
DateTime get time =>
DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000);
final double value;
}

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

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

View File

@ -16,47 +16,51 @@ import 'config/localization.dart';
import 'logic/cubit/app_settings/app_settings_cubit.dart'; import 'logic/cubit/app_settings/app_settings_cubit.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized();
await HiveConfig.init(); await HiveConfig.init();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
Bloc.observer = SimpleBlocObserver(); Bloc.observer = SimpleBlocObserver();
Wakelock.enable(); Wakelock.enable();
await getItSetup(); await getItSetup();
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized(); await EasyLocalization.ensureInitialized();
runApp( runApp(MyApp());
Localization(
child: BlocAndProviderConfig(
child: MyApp(),
),
),
);
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { 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>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light, // Manually changnig appbar color value: SystemUiOverlayStyle.light, // Manually changnig appbar color
child: MaterialApp( child: MaterialApp(
navigatorKey: getIt.get<NavigationService>().navigatorKey, scaffoldMessengerKey:
localizationsDelegates: context.localizationDelegates, getIt.get<NavigationService>().scaffoldMessengerKey,
supportedLocales: context.supportedLocales, navigatorKey: getIt.get<NavigationService>().navigatorKey,
locale: context.locale, localizationsDelegates: context.localizationDelegates,
debugShowCheckedModeBanner: false, supportedLocales: context.supportedLocales,
title: 'SelfPrivacy', locale: context.locale,
theme: appSettings.isDarkModeOn ? darkTheme : ligtTheme, debugShowCheckedModeBanner: false,
home: appSettings.isOnbordingShowing title: 'SelfPrivacy',
? OnboardingPage(nextPage: InitializingPage()) theme: appSettings.isDarkModeOn ? darkTheme : ligtTheme,
: RootPage(), home: appSettings.isOnbordingShowing
builder: (BuildContext context, Widget? widget) { ? OnboardingPage(nextPage: InitializingPage())
Widget error = Text('...rendering error...'); : RootPage(),
if (widget is Scaffold || widget is Navigator) builder: (BuildContext context, Widget? widget) {
error = Scaffold(body: Center(child: error)); Widget error = Text('...rendering error...');
ErrorWidget.builder = (FlutterErrorDetails errorDetails) => error; if (widget is Scaffold || widget is Navigator)
return widget!; 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({ static rised({
Key? key, Key? key,
required VoidCallback? onPressed, required VoidCallback? onPressed,
String? title, String? text,
Widget? child, Widget? child,
}) { }) {
assert(title == null || child == null, 'required title or child'); assert(text == 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');
return _RisedButton( return _RisedButton(
key: key, key: key,
title: title, title: text,
onPressed: onPressed, onPressed: onPressed,
child: child, 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:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.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/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/pre_styled_buttons/pre_styled_buttons.dart';
class BrandHeader extends StatelessWidget { class BrandHeader extends StatelessWidget {
const BrandHeader({ const BrandHeader({
Key? key, Key? key,
required this.title, required this.title,
this.hasBackButton = false, this.hasBackButton = false,
this.hasFlashButton = false,
}) : super(key: key); }) : super(key: key);
final String title; final String title;
final bool hasBackButton; final bool hasBackButton;
final bool hasFlashButton;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -31,6 +34,8 @@ class BrandHeader extends StatelessWidget {
SizedBox(width: 10), SizedBox(width: 10),
], ],
BrandText.h4(title), 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

@ -39,7 +39,9 @@ class _BrandMarkdownState extends State<BrandMarkdown> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var isDark = Theme.of(context).brightness == Brightness.dark; var isDark = Theme.of(context).brightness == Brightness.dark;
var markdown = MarkdownStyleSheet( var markdown = MarkdownStyleSheet(
p: defaultTextStyle, p: defaultTextStyle.copyWith(
color: isDark ? BrandColors.white : null,
),
h1: headline1Style.copyWith( h1: headline1Style.copyWith(
color: isDark ? BrandColors.white : null, color: isDark ? BrandColors.white : null,
), ),
@ -54,6 +56,7 @@ class _BrandMarkdownState extends State<BrandMarkdown> {
), ),
); );
return Markdown( return Markdown(
shrinkWrap: true,
styleSheet: markdown, styleSheet: markdown,
onTapLink: (String text, String? href, String title) { onTapLink: (String text, String? href, String title) {
if (href != null) { 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 { // class BrandModalSheet extends StatelessWidget {
const BrandModalSheet({ // const BrandModalSheet({
Key? key, // Key? key,
this.child, // this.child,
}) : super(key: key); // }) : super(key: key);
final Widget? child; // final Widget? child;
@override // @override
Widget build(BuildContext context) { // Widget build(BuildContext context) {
return DraggableScrollableSheet( // return DraggableScrollableSheet(
minChildSize: 0.95, // minChildSize: 1,
initialChildSize: 1, // initialChildSize: 1,
maxChildSize: 1, // maxChildSize: 1,
builder: (context, scrollController) { // builder: (context, scrollController) {
return SingleChildScrollView( // return SingleChildScrollView(
controller: scrollController, // controller: scrollController,
physics: ClampingScrollPhysics(), // physics: ClampingScrollPhysics(),
child: Container( // child: Container(
child: Column( // child: Column(
children: [ // children: [
GestureDetector( // GestureDetector(
onTap: () => Navigator.of(context).pop(), // onTap: () => Navigator.of(context).pop(),
behavior: HitTestBehavior.opaque, // behavior: HitTestBehavior.opaque,
child: Container( // child: Container(
width: double.infinity, // width: double.infinity,
child: Center( // child: Center(
child: Padding( // child: Padding(
padding: EdgeInsets.only(top: 132, bottom: 6), // padding: EdgeInsets.only(top: 132, bottom: 6),
child: Container( // child: Container(
height: 4, // height: 4,
width: 30, // width: 30,
decoration: BoxDecoration( // decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2), // borderRadius: BorderRadius.circular(2),
color: Color(0xFFE3E3E3).withOpacity(0.65), // color: Color(0xFFE3E3E3).withOpacity(0.65),
), // ),
), // ),
), // ),
), // ),
), // ),
), // ),
Container( // Container(
constraints: BoxConstraints( // constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height - 132, // minHeight: MediaQuery.of(context).size.height - 132,
maxHeight: MediaQuery.of(context).size.height - 132, // maxHeight: MediaQuery.of(context).size.height - 132,
), // ),
decoration: BoxDecoration( // decoration: BoxDecoration(
borderRadius: // borderRadius:
BorderRadius.vertical(top: Radius.circular(20)), // BorderRadius.vertical(top: Radius.circular(20)),
color: Theme.of(context).scaffoldBackgroundColor, // color: Theme.of(context).scaffoldBackgroundColor,
), // ),
width: double.infinity, // width: double.infinity,
child: child), // child: child),
], // ],
), // ),
), // ),
); // );
}); // });
} // }
} // }

View File

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
class BrandRadio extends StatelessWidget {
BrandRadio({
Key? key,
required this.isChecked,
}) : super(key: key);
final bool isChecked;
@override
Widget build(BuildContext context) {
return Container(
height: 20,
width: 20,
alignment: Alignment.center,
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: _getBorder(),
),
child: isChecked
? Container(
height: 10,
width: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: BrandColors.primary,
),
)
: null,
);
}
BoxBorder? _getBorder() {
return Border.all(
color: isChecked ? BrandColors.primary : BrandColors.gray1,
width: 2,
);
}
}

View File

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_radio/brand_radio.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
class BrandRadioTile extends StatelessWidget {
const BrandRadioTile({
Key? key,
required this.isChecked,
required this.text,
required this.onPress,
}) : super(key: key);
final bool isChecked;
final String text;
final VoidCallback onPress;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPress,
behavior: HitTestBehavior.translucent,
child: Padding(
padding: EdgeInsets.all(2),
child: Row(
children: [
BrandRadio(
isChecked: isChecked,
),
SizedBox(width: 9),
BrandText.h5(text)
],
),
),
);
}
}

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

View File

@ -70,9 +70,9 @@ class _BrandTimerState extends State<BrandTimer> {
_durationToString(DateTime.now().difference(widget.startDateTime)); _durationToString(DateTime.now().difference(widget.startDateTime));
String _durationToString(Duration duration) { String _durationToString(Duration duration) {
var timeLeft = widget.duration - duration;
String twoDigits(int n) => n.toString().padLeft(2, "0"); String twoDigits(int n) => n.toString().padLeft(2, "0");
String twoDigitSeconds = String twoDigitSeconds = twoDigits(timeLeft.inSeconds);
twoDigits(widget.duration.inSeconds - duration.inSeconds.remainder(60));
return "timer.sec".tr(args: [twoDigitSeconds]); return "timer.sec".tr(args: [twoDigitSeconds]);
} }

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

@ -33,9 +33,7 @@ class NotReadyCard extends StatelessWidget {
child: Text( child: Text(
'not_ready_card.2'.tr(), 'not_ready_card.2'.tr(),
style: body1Style.copyWith( style: body1Style.copyWith(
color: Theme.of(context).brightness == Brightness.dark color: Colors.white,
? Colors.black
: BrandColors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
// height: 1.1, // height: 1.1,

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_divider/brand_divider.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:easy_localization/easy_localization.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 { class OnePage extends StatelessWidget {
const OnePage({ const OnePage({
@ -16,33 +16,31 @@ class OnePage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return Scaffold(
child: Scaffold( appBar: PreferredSize(
appBar: PreferredSize( child: Column(
child: Column( children: [
children: [ Container(
Container( height: 51,
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,
alignment: Alignment.center, alignment: Alignment.center,
child: PreStyledButtons.close( padding: EdgeInsets.symmetric(horizontal: 15),
onPress: () => Navigator.of(context).pop()), 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'; part of 'pre_styled_buttons.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);
}
class _CloseButton extends StatelessWidget { class _CloseButton extends StatelessWidget {
const _CloseButton({Key? key, required this.onPress}) : super(key: key); 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, width: 10,
), ),
); );
even.add( odd.add(
SizedBox( 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

@ -1,5 +1,6 @@
import 'package:cubit_form/cubit_form.dart'; import 'package:cubit_form/cubit_form.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/forms/initializing/backblaze_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/initializing/backblaze_form_cubit.dart';
@ -9,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/forms/initializing/root_user_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_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/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_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_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_text/brand_text.dart';
import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart'; import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart';
import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart'; import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart';
@ -35,53 +36,63 @@ class InitializingPage extends StatelessWidget {
() => _stepCheck(cubit), () => _stepCheck(cubit),
() => _stepCheck(cubit), () => _stepCheck(cubit),
() => _stepCheck(cubit), () => _stepCheck(cubit),
() => Container(child: Text('Everythigng is initialized')) () => Container(child: Center(child: Text('initializing.finish'.tr())))
][cubit.state.progress](); ][cubit.state.progress]();
return BlocListener<AppConfigCubit, AppConfigState>( return BlocListener<AppConfigCubit, AppConfigState>(
listener: (context, state) { listener: (context, state) {
if (state.isFullyInitilized) { if (cubit.state is AppConfigFinished) {
Navigator.of(context).pushReplacement(materialRoute(RootPage())); Navigator.of(context).pushReplacement(materialRoute(RootPage()));
} }
}, },
child: SafeArea( child: SafeArea(
child: Scaffold( child: Scaffold(
body: ListView( body: SingleChildScrollView(
children: [ child: Column(
Padding( children: [
padding: brandPagePadding2.copyWith(top: 10, bottom: 10), Padding(
child: ProgressBar( padding: paddingH15V0.copyWith(top: 10, bottom: 10),
steps: [ child: ProgressBar(
'Hetzner', steps: [
'CloudFlare', 'Hetzner',
'Backblaze', 'CloudFlare',
'Domain', 'Backblaze',
'User', 'Domain',
'Server', 'User',
'', 'Server',
'', '✅ Check',
'', ],
'', activeIndex: cubit.state.porgressBar,
], ),
activeIndex: cubit.state.progress,
), ),
), _addCard(
_addCard( AnimatedSwitcher(
AnimatedSwitcher( duration: Duration(milliseconds: 300),
duration: Duration(milliseconds: 300), child: actualPage,
child: actualPage, ),
), ),
), ConstrainedBox(
BrandButton.text( constraints: BoxConstraints(
title: cubit.state.isFullyInitilized minHeight: MediaQuery.of(context).size.height -
? 'basis.close'.tr() MediaQuery.of(context).padding.top -
: 'basis.later'.tr(), MediaQuery.of(context).padding.bottom -
onPressed: () { 566,
Navigator.of(context).pushAndRemoveUntil( ),
materialRoute(RootPage()), child: Container(
(predicate) => false, alignment: Alignment.center,
); child: BrandButton.text(
}), title: cubit.state is AppConfigFinished
], ? 'basis.close'.tr()
: 'basis.later'.tr(),
onPressed: () {
Navigator.of(context).pushAndRemoveUntil(
materialRoute(RootPage()),
(predicate) => false,
);
},
),
)),
],
),
), ),
), ),
), ),
@ -118,7 +129,7 @@ class InitializingPage extends StatelessWidget {
onPressed: formCubitState.isSubmitting onPressed: formCubitState.isSubmitting
? null ? null
: () => context.read<HetznerFormCubit>().trySubmit(), : () => context.read<HetznerFormCubit>().trySubmit(),
title: 'basis.connect'.tr(), text: 'basis.connect'.tr(),
), ),
SizedBox(height: 10), SizedBox(height: 10),
BrandButton.text( BrandButton.text(
@ -173,7 +184,7 @@ class InitializingPage extends StatelessWidget {
onPressed: formCubitState.isSubmitting onPressed: formCubitState.isSubmitting
? null ? null
: () => context.read<CloudFlareFormCubit>().trySubmit(), : () => context.read<CloudFlareFormCubit>().trySubmit(),
title: 'basis.connect'.tr(), text: 'basis.connect'.tr(),
), ),
SizedBox(height: 10), SizedBox(height: 10),
BrandButton.text( BrandButton.text(
@ -224,7 +235,7 @@ class InitializingPage extends StatelessWidget {
onPressed: formCubitState.isSubmitting onPressed: formCubitState.isSubmitting
? null ? null
: () => context.read<BackblazeFormCubit>().trySubmit(), : () => context.read<BackblazeFormCubit>().trySubmit(),
title: 'basis.connect'.tr(), text: 'basis.connect'.tr(),
), ),
SizedBox(height: 10), SizedBox(height: 10),
BrandButton.text( BrandButton.text(
@ -316,7 +327,7 @@ class InitializingPage extends StatelessWidget {
SizedBox(height: 30), SizedBox(height: 30),
BrandButton.rised( BrandButton.rised(
onPressed: () => context.read<DomainSetupCubit>().saveDomain(), onPressed: () => context.read<DomainSetupCubit>().saveDomain(),
title: 'initializing.10'.tr(), text: 'initializing.10'.tr(),
), ),
], ],
SizedBox(height: 10), SizedBox(height: 10),
@ -341,8 +352,10 @@ class InitializingPage extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Spacer(), BrandText.h2('initializing.22'.tr()),
SizedBox(height: 10), SizedBox(height: 10),
BrandText.body2('initializing.23'.tr()),
Spacer(),
CubitFormTextField( CubitFormTextField(
formFieldCubit: context.read<RootUserFormCubit>().userName, formFieldCubit: context.read<RootUserFormCubit>().userName,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -384,7 +397,7 @@ class InitializingPage extends StatelessWidget {
onPressed: formCubitState.isSubmitting onPressed: formCubitState.isSubmitting
? null ? null
: () => context.read<RootUserFormCubit>().trySubmit(), : () => context.read<RootUserFormCubit>().trySubmit(),
title: 'basis.connect'.tr(), text: 'basis.connect'.tr(),
), ),
SizedBox(height: 10), SizedBox(height: 10),
BrandButton.text( BrandButton.text(
@ -398,7 +411,7 @@ class InitializingPage extends StatelessWidget {
} }
Widget _stepServer(AppConfigCubit appConfigCubit) { Widget _stepServer(AppConfigCubit appConfigCubit) {
var isLoading = appConfigCubit.state.isLoading; var isLoading = (appConfigCubit.state as AppConfigNotFinished).isLoading;
return Builder(builder: (context) { return Builder(builder: (context) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -409,10 +422,10 @@ class InitializingPage extends StatelessWidget {
BrandText.body2('initializing.11'.tr()), BrandText.body2('initializing.11'.tr()),
Spacer(), Spacer(),
BrandButton.rised( BrandButton.rised(
onPressed: isLoading! onPressed: isLoading
? null ? null
: () => appConfigCubit.createServerAndSetDnsRecords(), : () => appConfigCubit.createServerAndSetDnsRecords(),
title: isLoading ? 'basis.loading'.tr() : 'initializing.11'.tr(), text: isLoading ? 'basis.loading'.tr() : 'initializing.11'.tr(),
), ),
Spacer(flex: 2), Spacer(flex: 2),
BrandButton.text( BrandButton.text(
@ -425,28 +438,36 @@ class InitializingPage extends StatelessWidget {
} }
Widget _stepCheck(AppConfigCubit appConfigCubit) { Widget _stepCheck(AppConfigCubit appConfigCubit) {
assert(appConfigCubit.state is TimerState, 'wronge state'); assert(appConfigCubit.state is AppConfigNotFinished, 'wronge state');
var state = appConfigCubit.state as TimerState; var state = appConfigCubit.state as TimerState;
late int doneCount;
late String? text; late String? text;
if (state.isServerResetedSecondTime) { if (state.isServerResetedSecondTime) {
text = 'initializing.13'.tr(); text = 'initializing.13'.tr();
doneCount = 3;
} else if (state.isServerResetedFirstTime) { } else if (state.isServerResetedFirstTime) {
text = 'initializing.21'.tr(); text = 'initializing.21'.tr();
doneCount = 2;
} else if (state.isServerStarted) { } else if (state.isServerStarted) {
text = 'initializing.14'.tr(); text = 'initializing.14'.tr();
doneCount = 1;
} else if (state.isServerCreated) { } else if (state.isServerCreated) {
text = 'initializing.15'.tr(); text = 'initializing.15'.tr();
doneCount = 0;
} }
return Builder(builder: (context) { return Builder(builder: (context) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox(height: 15),
BrandText.h4(
'initializing.checks'.tr(args: [doneCount.toString(), "4"]),
),
Spacer(flex: 2), Spacer(flex: 2),
SizedBox(height: 10), SizedBox(height: 10),
BrandText.body2(text), BrandText.body2(text),
SizedBox(height: 10), SizedBox(height: 10),
if (!state.isLoading!) if (!state.isLoading)
Row( Row(
children: [ children: [
BrandText.body2('initializing.16'.tr()), BrandText.body2('initializing.16'.tr()),
@ -456,7 +477,7 @@ class InitializingPage extends StatelessWidget {
) )
], ],
), ),
if (state.isLoading!) BrandText.body2('initializing.17'.tr()), if (state.isLoading) BrandText.body2('initializing.17'.tr()),
Spacer( Spacer(
flex: 2, flex: 2,
), ),
@ -472,8 +493,8 @@ class InitializingPage extends StatelessWidget {
Widget _addCard(Widget child) { Widget _addCard(Widget child) {
return Container( return Container(
height: 450, height: 450,
padding: brandPagePadding2, padding: paddingH15V0,
child: BrandCard(child: child), child: BrandCards.big(child: child),
); );
} }
} }
@ -485,12 +506,14 @@ class _HowHetzner extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BrandModalSheet( return BrandBottomSheet(
isExpended: true,
child: Padding( child: Padding(
padding: brandPagePadding2.copyWith(top: 25), padding: paddingH15V0,
child: BrandMarkdown( child: BrandMarkdown(
fileName: 'how_hetzner', 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_alert/brand_alert.dart';
import 'package:selfprivacy/ui/components/brand_divider/brand_divider.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_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/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/utils/named_font_weight.dart'; import 'package:selfprivacy/utils/named_font_weight.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -27,12 +28,12 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
child: Builder(builder: (context) { child: Builder(builder: (context) {
return Scaffold( return Scaffold(
appBar: PreferredSize( appBar: PreferredSize(
child: child: BrandHeader(
BrandHeader(title: 'more.settings.title'.tr(), hasBackButton: true), title: 'more.settings.title'.tr(), hasBackButton: true),
preferredSize: Size.fromHeight(52), preferredSize: Size.fromHeight(52),
), ),
body: ListView( body: ListView(
padding: brandPagePadding2, padding: paddingH15V0,
children: [ children: [
BrandDivider(), BrandDivider(),
Container( Container(
@ -52,9 +53,7 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
), ),
), ),
SizedBox(width: 5), SizedBox(width: 5),
Switch( BrandSwitch(
activeColor: BrandColors.green1,
activeTrackColor: BrandColors.green2,
value: Theme.of(context).brightness == Brightness.dark, value: Theme.of(context).brightness == Brightness.dark,
onChanged: (value) => context onChanged: (value) => context
.read<AppSettingsCubit>() .read<AppSettingsCubit>()
@ -116,16 +115,89 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
}, },
); );
}, },
) ),
], ],
), ),
) ),
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 { class _TextColumn extends StatelessWidget {

View File

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

View File

@ -1,15 +1,29 @@
import 'package:flutter/material.dart'; 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_colors.dart';
import 'package:selfprivacy/config/brand_theme.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_divider/brand_divider.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.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_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.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/initializing/initializing.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
import 'package:selfprivacy/ui/pages/rootRoute.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:share_plus/share_plus.dart';
import 'about/about.dart'; import 'about/about.dart';
import 'app_settings/app_setting.dart'; import 'app_settings/app_setting.dart';
@ -21,15 +35,21 @@ class MorePage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var jobsCubit = context.watch<JobsCubit>();
var isReady = context.watch<AppConfigCubit>().state is AppConfigFinished;
return Scaffold( return Scaffold(
appBar: PreferredSize( appBar: PreferredSize(
child: BrandHeader(title: 'basis.more'.tr()), child: BrandHeader(
title: 'basis.more'.tr(),
hasFlashButton: true,
),
preferredSize: Size.fromHeight(52), preferredSize: Size.fromHeight(52),
), ),
body: ListView( body: ListView(
children: [ children: [
Padding( Padding(
padding: brandPagePadding2, padding: paddingH15V0,
child: Column( child: Column(
children: [ children: [
BrandDivider(), BrandDivider(),
@ -63,6 +83,73 @@ class MorePage extends StatelessWidget {
iconData: BrandIcons.terminal, iconData: BrandIcons.terminal,
goTo: Console(), 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 { class _NavItem extends StatelessWidget {
const _NavItem({ const _NavItem({
Key? key, Key? key,
@ -88,29 +319,81 @@ class _NavItem extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () => Navigator.of(context).push(materialRoute(goTo)), onTap: () => Navigator.of(context).push(materialRoute(goTo)),
child: Container( child: _MoreMenuItem(
padding: EdgeInsets.symmetric(vertical: 24), iconData: iconData,
decoration: BoxDecoration( title: title,
border: Border( isActive: true,
bottom: BorderSide( ),
width: 1.0, );
color: BrandColors.dividerColor, }
), }
),
), class _MoreMenuTapItem extends StatelessWidget {
child: Row( const _MoreMenuTapItem({
children: [ Key? key,
BrandText.body1(title), required this.iconData,
Spacer(), required this.onTap,
SizedBox( required this.title,
width: 56, }) : super(key: key);
child: Icon(
iconData, final IconData iconData;
size: 20, 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, curve: Curves.easeIn,
); );
}, },
title: 'basis.next'.tr(), text: 'basis.next'.tr(),
), ),
SizedBox(height: 30), SizedBox(height: 30),
], ],
@ -129,7 +129,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
(route) => false, (route) => false,
); );
}, },
title: 'basis.got_it'.tr(), text: 'basis.got_it'.tr(),
), ),
SizedBox(height: 30), SizedBox(height: 30),
], ],

View File

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

View File

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

View File

@ -0,0 +1,167 @@
part of 'server_details.dart';
class _Chart extends StatelessWidget {
const _Chart({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
var cubit = context.watch<HetznerMetricsCubit>();
var period = cubit.state.period;
var state = cubit.state;
List<Widget> charts;
if (state is HetznerMetricsLoading) {
charts = [
Container(
height: 200,
alignment: Alignment.center,
child: Text('basis.loading'.tr()),
)
];
} else if (state is HetznerMetricsLoaded) {
charts = [
Legend(color: Colors.red, text: 'CPU %'),
getCpuChart(state),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
BrandText.small('Public Network interface packets per sec'),
SizedBox(width: 10),
Legend(color: Colors.red, text: 'IN'),
SizedBox(width: 5),
Legend(color: Colors.green, text: 'OUT'),
],
),
getPpsChart(state),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
BrandText.small('Public Network interface bytes per sec'),
SizedBox(width: 10),
Legend(color: Colors.red, text: 'IN'),
SizedBox(width: 5),
Legend(color: Colors.green, text: 'OUT'),
],
),
getBandwidthChart(state),
];
} else {
throw 'wrong state';
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: Column(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
BrandRadioTile(
isChecked: period == Period.month,
text: 'providers.server.chart.month'.tr(),
onPress: () => cubit.changePeriod(Period.month),
),
BrandRadioTile(
isChecked: period == Period.day,
text: 'providers.server.chart.day'.tr(),
onPress: () => cubit.changePeriod(Period.day),
),
BrandRadioTile(
isChecked: period == Period.hour,
text: 'providers.server.chart.hour'.tr(),
onPress: () => cubit.changePeriod(Period.hour),
),
],
),
),
...charts,
],
),
);
}
Widget getCpuChart(HetznerMetricsLoaded state) {
var data = state.cpu;
return Container(
height: 200,
child: CpuChart(data, state.period, state.start),
);
}
Widget getPpsChart(HetznerMetricsLoaded state) {
var ppsIn = state.ppsIn;
var ppsOut = state.ppsOut;
return Container(
height: 200,
child: NetworkChart(
[ppsIn, ppsOut],
state.period,
state.start,
),
);
}
Widget getBandwidthChart(HetznerMetricsLoaded state) {
var ppsIn = state.bandwidthIn;
var ppsOut = state.bandwidthOut;
return Container(
height: 200,
child: NetworkChart(
[ppsIn, ppsOut],
state.period,
state.start,
),
);
}
}
class Legend extends StatelessWidget {
const Legend({
Key? key,
required this.color,
required this.text,
}) : super(key: key);
final String text;
final Color color;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_ColoredBox(color: color),
SizedBox(width: 5),
BrandText.small(text),
],
);
}
}
class _ColoredBox extends StatelessWidget {
const _ColoredBox({
Key? key,
required this.color,
}) : super(key: key);
final Color color;
@override
Widget build(BuildContext context) {
return Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: color.withOpacity(0.3),
border: Border.all(
color: color,
)),
);
}
}

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