add the ability to import themes and support for loading custom themes that aren't assets

This commit is contained in:
Dan Ballard 2024-02-01 23:48:20 -08:00 committed by Gitea
parent 183b88d8e0
commit a8c957e679
34 changed files with 497 additions and 128 deletions

View File

@ -61,6 +61,8 @@ themes:
textfieldHintColor: mainTextColor
toolbarIconColor: settings # whiteishPurple
topbarColor: header # darkGreyPurple
snackbarBackgroundColor: accent
snackbarTextColor: whiteishPurple
chatImageColor: purple
light:
colors:

View File

@ -14,6 +14,8 @@ abstract class Cwtch {
// ignore: non_constant_identifier_names
Future<void> ReconnectCwtchForeground();
Future<String> getCwtchDir();
String getAssetsDir();
// ignore: non_constant_identifier_names

View File

@ -135,6 +135,7 @@ class CwtchFfi implements Cwtch {
ReceivePort _receivePort = ReceivePort();
bool _isL10nInit = false;
String _assetsDir = path.join(Directory.current.path, "data", "flutter_assets");
String _cwtchDir = "";
static String getLibraryPath() {
if (Platform.isWindows) {
@ -169,13 +170,12 @@ class CwtchFfi implements Cwtch {
String home = "";
String bundledTor = "";
Map<String, String> envVars = Platform.environment;
String cwtchDir = "";
if (Platform.isLinux) {
home = envVars['HOME'] ?? "";
if (EnvironmentConfig.TEST_MODE) {
cwtchDir = envVars['CWTCH_HOME']!;
_cwtchDir = envVars['CWTCH_HOME']!;
} else {
cwtchDir = envVars['CWTCH_HOME'] ?? path.join(envVars['HOME']!, ".cwtch");
_cwtchDir = envVars['CWTCH_HOME'] ?? path.join(envVars['HOME']!, ".cwtch");
}
if (await File("linux/Tor/tor").exists()) {
@ -192,7 +192,7 @@ class CwtchFfi implements Cwtch {
bundledTor = "tor";
}
} else if (Platform.isWindows) {
cwtchDir = envVars['CWTCH_DIR'] ?? path.join(envVars['UserProfile']!, ".cwtch");
_cwtchDir = envVars['CWTCH_DIR'] ?? path.join(envVars['UserProfile']!, ".cwtch");
String currentTor = path.join(Directory.current.absolute.path, "Tor\\Tor\\tor.exe");
if (await File(currentTor).exists()) {
bundledTor = currentTor;
@ -203,7 +203,7 @@ class CwtchFfi implements Cwtch {
_assetsDir = path.join(exeDir, "data", "flutter_assets");
}
} else if (Platform.isMacOS) {
cwtchDir = envVars['CWTCH_HOME'] ?? path.join(envVars['HOME']!, "Library/Application Support/Cwtch");
_cwtchDir = envVars['CWTCH_HOME'] ?? path.join(envVars['HOME']!, "Library/Application Support/Cwtch");
_assetsDir = "/Applications/Cwtch.app/Contents/Frameworks/App.framework/Versions/Current/Resources/flutter_assets/";
if (await File("Cwtch.app/Contents/MacOS/Tor/tor").exists()) {
bundledTor = "Cwtch.app/Contents/MacOS/Tor/tor";
@ -232,27 +232,27 @@ class CwtchFfi implements Cwtch {
// if macOs and release build and no profile and is dev profile
// copy dev profile to release profile
if (Platform.isMacOS && EnvironmentConfig.BUILD_VER != dev_version) {
var devProfileExists = await Directory(path.join(cwtchDir, "dev", "profiles")).exists();
var releaseProfileExists = await Directory(path.join(cwtchDir, "profiles")).exists();
var devProfileExists = await Directory(path.join(_cwtchDir, "dev", "profiles")).exists();
var releaseProfileExists = await Directory(path.join(_cwtchDir, "profiles")).exists();
if (devProfileExists && !releaseProfileExists) {
print("MacOS one time dev -> release profile migration...");
await Process.run("cp", ["-r", "-p", path.join(cwtchDir, "dev", "profiles"), cwtchDir]);
await Process.run("cp", ["-r", "-p", path.join(cwtchDir, "dev", "SALT"), cwtchDir]);
await Process.run("cp", ["-r", "-p", path.join(cwtchDir, "dev", "ui.globals"), cwtchDir]);
await Process.run("cp", ["-r", "-p", path.join(_cwtchDir, "dev", "profiles"), _cwtchDir]);
await Process.run("cp", ["-r", "-p", path.join(_cwtchDir, "dev", "SALT"), _cwtchDir]);
await Process.run("cp", ["-r", "-p", path.join(_cwtchDir, "dev", "ui.globals"), _cwtchDir]);
}
}
if (EnvironmentConfig.BUILD_VER == dev_version) {
cwtchDir = path.join(cwtchDir, "dev");
_cwtchDir = path.join(_cwtchDir, "dev");
}
print("StartCwtch( cwtchdir: $cwtchDir, torPath: $bundledTor )");
print("StartCwtch( cwtchdir: $_cwtchDir, torPath: $bundledTor )");
var startCwtchC = library.lookup<NativeFunction<start_cwtch_function>>("c_StartCwtch");
// ignore: non_constant_identifier_names
final StartCwtch = startCwtchC.asFunction<StartCwtchFn>();
final utf8CwtchDir = cwtchDir.toNativeUtf8();
final utf8CwtchDir = _cwtchDir.toNativeUtf8();
StartCwtch(utf8CwtchDir, utf8CwtchDir.length, bundledTor.toNativeUtf8(), bundledTor.length);
malloc.free(utf8CwtchDir);
@ -268,6 +268,10 @@ class CwtchFfi implements Cwtch {
return _assetsDir;
}
Future<String> getCwtchDir() async {
return _cwtchDir;
}
// ignore: non_constant_identifier_names
Future<void> ReconnectCwtchForeground() async {
var reconnectCwtch = library.lookup<NativeFunction<Void Function()>>("c_ReconnectCwtchForeground");

View File

@ -30,6 +30,7 @@ class CwtchGomobile implements Cwtch {
late Future<dynamic> androidLibraryDir;
late Future<dynamic> androidHomeDirectory;
String androidHomeDirectoryStr = "";
String _cwtchDir = "";
late CwtchNotifier cwtchNotifier;
bool _isL10nInit = false;
@ -55,6 +56,8 @@ class CwtchGomobile implements Cwtch {
cwtchNotifier = _cwtchNotifier;
cwtchNotifier.setMessageSeenCallback((String profile, int conversation, DateTime time) => {this.SetConversationAttribute(profile, conversation, LastMessageSeenTimeKey, time.toIso8601String())});
androidHomeDirectory = getApplicationDocumentsDirectory();
androidLibraryDir = appInfoPlatform.invokeMethod('getNativeLibDir');
// Method channel to receive libcwtch-go events via Kotlin and dispatch them to _handleAppbusEvent (sends to cwtchNotifier)
@ -67,15 +70,20 @@ class CwtchGomobile implements Cwtch {
return "";
}
Future<String> getCwtchDir() async {
androidHomeDirectoryStr = (await androidHomeDirectory).path;
var _cwtchDir = path.join(androidHomeDirectoryStr, ".cwtch");
if (EnvironmentConfig.BUILD_VER == dev_version) {
_cwtchDir = path.join(_cwtchDir, "dev");
}
return androidHomeDirectoryStr;
}
// ignore: non_constant_identifier_names
Future<void> Start() async {
print("gomobile.dart: Start()...");
androidHomeDirectoryStr = (await androidHomeDirectory).path;
var cwtchDir = path.join(androidHomeDirectoryStr, ".cwtch");
if (EnvironmentConfig.BUILD_VER == dev_version) {
cwtchDir = path.join(cwtchDir, "dev");
}
String torPath = path.join(await androidLibraryDir, "libtor.so");
String cwtchDir = await getCwtchDir();
print("gomobile.dart: Start invokeMethod Start($cwtchDir, $torPath)...");
cwtchPlatform.invokeMethod("Start", {"appDir": cwtchDir, "torPath": torPath});
}

View File

@ -1,6 +1,12 @@
{
"@@locale": "cy",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "da",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "de",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "el",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "en",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingThemeOverwriteConfirm": "Confirm",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingsImportThemeTitle": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeButton": "Import Theme",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "es",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "fr",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "it",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "ja",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,36 @@
{
"@@locale": "ko",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"resetTor": "초기화",
"torStatus": "Tor 상태",
"torVersion": "Tor 버전",
"passwordChangeError": "암호 변경 오류: 제공된 암호가 거부되었습니다.",
"enterProfilePassword": "프로필을 보려면 암호를 입력하세요.",
"todoPlaceholder": "할 일...",
"addNewItem": "목록에 새 항목 추가",
"addListItem": "새 목록 항목 추가",
"networkStatusConnecting": "네트워크 및 연락처에 연결 중...",
"networkStatusAttemptingTor": "Tor 네트워크에 연결을 시도 중",
"networkStatusDisconnected": "인터넷 연결 끊김, 연결 확인하세요",
"viewGroupMembershipTooltip": "그룹 구성원 보기",
"defaultScalingText": "글꼴 크기 조정",
"experimentsEnabled": "실험 사용",
"loadingTor": "tor 로딩 중...",
"builddate": "빌드 대상: %2",
"version": "버전: %1",
"versionTor": "버전 %1 및 tor %2",
"settingInterfaceZoom": "확대\/축소 수준",
"versionBuilddate": "버전: %1 빌드 대상: %2",
"error0ProfilesLoadedForPassword": "해당 비밀번호가 포함된 프로필이 0개 로드되었습니다.",
"deleteConfirmText": "삭제",
"unblockBtn": "연락처 차단 해제",
"savePeerHistoryDescription": "연락처와 연결된 기록을 삭제할지 여부를 결정합니다.",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",
@ -116,7 +146,6 @@
"server": "서버",
"peerName": "이름",
"peerAddress": "주소",
"builddate": "Built on: %2",
"deleteProfileConfirmBtn": "프로필 삭제 확인",
"deleteConfirmLabel": "DELETE를 입력하여 확인",
"deleteProfileBtn": "프로필 삭제",
@ -152,7 +181,6 @@
"localeSw": "Swahili \/ Kiswahili",
"localeSv": "Swedish \/ Svenska",
"fontScalingDescription": "Adjust the relative font scaling factor applied to text and widgets.",
"defaultScalingText": "Font Scaling",
"localeJa": "Japanese \/ 日本語",
"retryConnectionTooltip": "Cwtch retries peers regularly, but you can tell Cwtch to try sooner by pushing this button.",
"retryConnection": "Retry",
@ -377,9 +405,6 @@
"addServerFirst": "You need to add a server before you can create a group",
"deleteProfileSuccess": "Successfully deleted profile",
"sendInvite": "Send a contact or group invite",
"resetTor": "Reset",
"torStatus": "Tor Status",
"torVersion": "Tor Version",
"sendAnInvitation": "You sent an invitation for: ",
"contactSuggestion": "This is a contact suggestion for: ",
"chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.",
@ -390,24 +415,5 @@
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.",
"invalidImportString": "Invalid import string",
"todoPlaceholder": "Todo...",
"addNewItem": "Add a new item to the list",
"addListItem": "Add a New List Item",
"networkStatusConnecting": "Connecting to network and contacts...",
"networkStatusAttemptingTor": "Attempting to connect to Tor network",
"networkStatusDisconnected": "Disconnected from the internet, check your connection",
"viewGroupMembershipTooltip": "View Group Membership",
"loadingTor": "Loading tor...",
"version": "Version %1",
"versionTor": "Version %1 with tor %2",
"experimentsEnabled": "Enable Experiments",
"settingInterfaceZoom": "Zoom level",
"versionBuilddate": "Version: %1 Built on: %2",
"error0ProfilesLoadedForPassword": "0 profiles loaded with that password",
"enterProfilePassword": "Enter a password to view your profiles",
"deleteConfirmText": "DELETE",
"passwordChangeError": "Error changing password: Supplied password rejected",
"unblockBtn": "Unblock Contact",
"savePeerHistoryDescription": "Determines whether to delete any history associated with the contact.",
"pasteAddressToAddContact": "Paste a cwtch address, invitation or key bundle here to add a new conversation"
}

View File

@ -1,6 +1,12 @@
{
"@@locale": "lb",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "nl",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "no",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "pl",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "pt",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "pt_BR",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "ro",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "ru",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "sk",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "sv",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "sw",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "tr",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "uk",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -1,6 +1,12 @@
{
"@@locale": "uz",
"@@last_modified": "2024-01-10T06:54:15+01:00",
"@@last_modified": "2024-02-02T08:42:10+01:00",
"settingsImportThemeButton": "Import Theme",
"settingsImportThemeDescription": "Select theme directory to import for use in Cwtch",
"settingsImportThemeTitle": "Import Theme",
"settingsThemeErrorInvalid": "Error: Could not import $themeName, theme.yml missing, not a theme directory",
"settingThemeOverwriteQuestion": "Theme $themeName already exists, confirm overwrite?",
"settingThemeOverwriteConfirm": "Confirm",
"settingsThemeImagesDescription": "Enable display of images from themes",
"settingsThemeImages": "Theme Images",
"settingsGroupAbout": "About",

View File

@ -109,10 +109,10 @@ class FlwtchState extends State<Flwtch> with WindowListener {
}
print("initState: invoking cwtch.Start()");
// Cwtch.start can take time, we don't want it blocking first splash screen draw, so postpone a smidge to let splash render
Future.delayed(const Duration(milliseconds: 50), () {
Future.delayed(const Duration(milliseconds: 50), () async {
print("actually invoking cwtch.cwtch()!!!");
cwtch.Start();
LoadAssetThemes();
await cwtch.Start();
LoadThemes(await cwtch.getCwtchDir());
});
print("initState: starting connectivityListener");
if (EnvironmentConfig.TEST_MODE == false) {

View File

@ -81,6 +81,8 @@ class CwtchDark extends OpaqueThemeType {
get textfieldHintColor => mainTextColor;
get toolbarIconColor => settings; //whiteishPurple;
get topbarColor => header; //darkGreyPurple;
get snackbarBackgroundColor => accent;
get snackbarTextColor => whitePurple;
get chatImageColor => purple;
}
@ -127,5 +129,7 @@ class CwtchLight extends OpaqueThemeType {
get textfieldHintColor => font;
get toolbarIconColor => settings; //darkPurple;
get topbarColor => header; //softPurple;
get snackbarBackgroundColor => accent;
get snackbarTextColor => whitePurple;
get chatImageColor => purple;
}

View File

@ -7,6 +7,9 @@ import 'package:cwtch/themes/yamltheme.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/settings.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as path;
const custom_themes_subdir = "themes";
const mode_light = "light";
const mode_dark = "dark";
@ -20,8 +23,10 @@ final TextStyle defaultDropDownMenuItemTextStyle = TextStyle(fontFamily: "Inter"
Map<String, Map<String, OpaqueThemeType>> themes = Map();
LoadAssetThemes() async {
themes = await loadYamlThemes();
LoadThemes(String cwtchDir) async {
themes = await loadBuiltinThemes();
final customThemes = await loadCustomThemes(path.join(cwtchDir, custom_themes_subdir));
themes.addAll(customThemes);
}
OpaqueThemeType getTheme(String themeId, String mode) {
@ -119,6 +124,9 @@ abstract class OpaqueThemeType {
get messageFromOtherBackgroundColor => red;
get messageFromOtherTextColor => red;
get snackbarBackgroundColor => red;
get snackbarTextColor => red;
// Images
get chatImageColor => red;
@ -241,5 +249,15 @@ ThemeData mkThemeData(Settings opaque) {
splashColor: opaque.current().defaultButtonActiveColor),
textSelectionTheme: TextSelectionThemeData(
cursorColor: opaque.current().defaultButtonActiveColor, selectionColor: opaque.current().defaultButtonActiveColor, selectionHandleColor: opaque.current().defaultButtonActiveColor),
popupMenuTheme: PopupMenuThemeData(
color: opaque
.current()
.backgroundPaneColor
.withOpacity(0.9),
),
snackBarTheme: SnackBarThemeData(
backgroundColor: opaque.current().snackbarBackgroundColor,
contentTextStyle: TextStyle(color: opaque.current().snackbarTextColor),
)
);
}

View File

@ -7,13 +7,14 @@ import 'package:cwtch/themes/cwtch.dart';
import 'package:cwtch/themes/opaque.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:glob/list_local_fs.dart';
import 'package:provider/provider.dart';
import 'package:yaml/yaml.dart';
import 'package:path/path.dart' as path;
import '../main.dart';
Future<Map<String, Map<String, OpaqueThemeType>>> loadYamlThemes() async {
Future<Map<String, Map<String, OpaqueThemeType>>> loadBuiltinThemes() async {
final manifestJson = await rootBundle.loadString('AssetManifest.json');
final themesList = json.decode(manifestJson).keys.where((String key) => key.startsWith('assets/themes'));
@ -25,7 +26,7 @@ Future<Map<String, Map<String, OpaqueThemeType>>> loadYamlThemes() async {
}
try {
var data = await loadYamlTheme(themefile);
var data = await loadAssetYamlTheme(themefile);
if (data != null) {
// remove "assets/themes" and "theme.yml" from name
@ -39,24 +40,71 @@ Future<Map<String, Map<String, OpaqueThemeType>>> loadYamlThemes() async {
return themes;
}
Future<Map<String, Map<String, OpaqueThemeType>>> loadCustomThemes(String themesDirPath) async {
Map<String, Map<String, OpaqueThemeType>> themes = Map();
Directory themesDir = Directory(themesDirPath);
if (! themesDir.existsSync()) {
themesDir = await themesDir.create();
}
await themesDir.list(recursive: false).forEach( (themeDir) async {
//final themeDir = Directory(path.join(themesDir, themedir));
File themefile = File(path.join(themeDir.path, "theme.yml"));
if (themefile.existsSync()) {
try {
var data = await loadFileYamlTheme(themefile.path, themeDir.path);
if (data != null) {
themes[path.basename(themeDir.path)] = data;
}
} catch (e) {
print("Failed to load theme: $themefile with exception: $e");
}
}
});
return themes;
}
Future<YamlMap?> readAssetYamlTheme(String themefile) async {
final contents = await rootBundle.loadString(themefile);
return loadYaml(contents);
}
Future<Map<String, OpaqueThemeType>?> loadYamlTheme(String themefile) async {
Future<Map<String, OpaqueThemeType>?> loadAssetYamlTheme(String themefile) async {
final yml = await readAssetYamlTheme(themefile);
if (yml == null) {
print("failed to load theme: $themefile");
print("Error: failed to load theme: $themefile");
return null;
}
return loadYamlTheme(yml);
}
Future<Map<String, OpaqueThemeType>?> loadFileYamlTheme(String themefile, String assetsDir) async {
final file = File(themefile);
if (!file.existsSync()) {
print("Error: theme file: $themefile does not exist");
return null;
}
final contents = file.readAsStringSync();
final yml = await loadYaml(contents);
if (yml == null) {
print("Error: failed to load theme: $themefile");
return null;
}
return loadYamlTheme(yml, assetsDir);
}
Future<Map<String, OpaqueThemeType>?> loadYamlTheme(YamlMap yml, [String? assetsDir]) async {
Map<String, OpaqueThemeType> subthemes = Map();
if ((yml["themes"] as YamlMap).containsKey(mode_dark)) {
subthemes[mode_dark] = YmlTheme(yml, yml["themes"]["name"], mode_dark);
subthemes[mode_dark] = YmlTheme(yml, yml["themes"]["name"], mode_dark, assetsDir);
}
if ((yml["themes"] as YamlMap).containsKey(mode_light)) {
subthemes[mode_light] = YmlTheme(yml, yml["themes"]["name"], mode_light);
subthemes[mode_light] = YmlTheme(yml, yml["themes"]["name"], mode_light, assetsDir);
}
return subthemes;
}
@ -65,12 +113,14 @@ class YmlTheme extends OpaqueThemeType {
late YamlMap yml;
late String mode;
late String theme;
late String? assetsDir;
late OpaqueThemeType fallbackTheme;
YmlTheme(YamlMap yml, theme, mode) {
YmlTheme(YamlMap yml, theme, mode, assetsDir) {
this.yml = yml;
this.theme = theme;
this.mode = mode;
this.assetsDir = assetsDir;
if (mode == mode_dark) {
fallbackTheme = CwtchDark();
} else {
@ -92,13 +142,22 @@ class YmlTheme extends OpaqueThemeType {
if (!(val is int)) {
return null;
}
return Color(0xFF000000 + val as int);
if ( val <= 0xFFFFFF) {
return Color(0xFF000000 + val);
} else {
return Color(val);
}
}
String? getImage(String name) {
var val = yml["themes"][mode]["theme"]?[name];
if (val != null) {
return path.join("assets", "themes", yml["themes"]["name"], val);
if (assetsDir == null) {
return path.join("assets", "themes", yml["themes"]["name"], val);
} else {
return val;
}
}
return null;
}
@ -137,6 +196,8 @@ class YmlTheme extends OpaqueThemeType {
get messageFromMeTextColor => getColor("messageFromMeTextColor") ?? fallbackTheme.messageFromMeTextColor;
get messageFromOtherBackgroundColor => getColor("messageFromOtherBackgroundColor") ?? fallbackTheme.messageFromOtherBackgroundColor;
get messageFromOtherTextColor => getColor("messageFromOtherTextColor") ?? fallbackTheme.messageFromOtherTextColor;
get snackbarBackgroundColor => getColor("snackbarBackgroundColor") ?? fallbackTheme.snackbarBackgroundColor;
get snackbarTextColor => getColor("snackbarTextColor") ?? fallbackTheme.snackbarTextColor;
// Images
@ -149,6 +210,14 @@ class YmlTheme extends OpaqueThemeType {
return FileImage(f);
}
final assetsDir = this.assetsDir;
if (assetsDir != null) {
File f = File(path.join(assetsDir, key));
if (f.existsSync()) {
return FileImage(f);
}
}
if (context != null) {
File af = File(path.join(Provider
.of<FlwtchState>(context, listen: false)

View File

@ -1,12 +1,20 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:path/path.dart' as path;
import '../config.dart';
import '../controllers/filesharing.dart';
import '../cwtch_icons_icons.dart';
import '../main.dart';
import '../models/appstate.dart';
import '../settings.dart';
import '../themes/cwtch.dart';
import '../themes/opaque.dart';
import '../themes/yamltheme.dart';
import '../widgets/folderpicker.dart';
import 'globalsettingsview.dart';
class GlobalSettingsAppearanceView extends StatefulWidget {
@ -15,7 +23,6 @@ class GlobalSettingsAppearanceView extends StatefulWidget {
}
class _GlobalSettingsAppearanceViewState extends State<GlobalSettingsAppearanceView> {
ScrollController settingsListScrollController = ScrollController();
Widget build(BuildContext context) {
@ -123,50 +130,66 @@ class _GlobalSettingsAppearanceViewState extends State<GlobalSettingsAppearanceV
.theme,
onChanged: (String? newValue) {
setState(() {
settings.setTheme(
newValue!, settings.theme.mode);
saveSettings(context);
});
},
items: themes.keys
.map<DropdownMenuItem<String>>(
(String themeId) {
return DropdownMenuItem<String>(
value: themeId,
child: Text(
getThemeName(context, themeId),
style: settings.scaleFonts(
defaultDropDownMenuItemTextStyle)), //"ddi_$themeId", key: Key("ddi_$themeId")),
);
}).toList())),
leading: Icon(Icons.palette,
color: settings
.current()
.mainTextColor),
),
SwitchListTile(
title: Text(
AppLocalizations.of(context)!.settingsThemeImages),
subtitle: Text(AppLocalizations.of(context)!
.settingsThemeImagesDescription),
value: settings.themeImages,
onChanged: (bool value) {
settings.themeImages = value;// Save Settings...
saveSettings(context);
},
activeTrackColor: settings.theme.defaultButtonColor,
inactiveTrackColor:
settings.theme.defaultButtonDisabledColor,
secondary: Icon(Icons.image,
color: settings
.current()
.mainTextColor),
),
ListTile(
title: Text(AppLocalizations.of(context)!
.settingUIColumnPortrait),
leading: Icon(Icons.table_chart,
color: settings
settings.setTheme(newValue!, settings.theme.mode);
saveSettings(context);
});
},
items: themes.keys.map<DropdownMenuItem<String>>((String themeId) {
return DropdownMenuItem<String>(
value: themeId,
child: Text(getThemeName(context, themeId), style: settings.scaleFonts(defaultDropDownMenuItemTextStyle)), //"ddi_$themeId", key: Key("ddi_$themeId")),
);
}).toList())),
leading: Icon(Icons.palette, color: settings.current().mainTextColor),
),
Visibility(
// TODO: Android support needs gomobile support for reading / writing themes, and ideally importing from a .zip or .tar.gz
visible: !Platform.isAndroid,
child: ListTile(
leading: Icon(Icons.palette, color: Provider.of<Settings>(context).theme.messageFromMeTextColor),
title: Text(AppLocalizations.of(context)!.settingsImportThemeTitle),
subtitle: Text(AppLocalizations.of(context)!.settingsImportThemeDescription),
//AppLocalizations.of(
//context)!
//.fileSharingSettingsDownloadFolderDescription,
trailing: Container(
width: MediaQuery.of(context).size.width / 4,
child: ElevatedButton.icon(
label: Text(AppLocalizations.of(context)!.settingsImportThemeButton),
onPressed: Provider.of<AppState>(context).disableFilePicker
? null
: () async {
if (Platform.isAndroid) {
return;
}
var selectedDirectory = await showSelectDirectoryPicker(context);
if (selectedDirectory != null) {
selectedDirectory += path.separator;
final customThemeDir = path.join(await Provider.of<FlwtchState>(context, listen: false).cwtch.getCwtchDir(), custom_themes_subdir);
importThemeCheck(context, settings, customThemeDir, selectedDirectory);
} else {
// User canceled the picker
}
},
//onChanged: widget.onSave,
icon: Icon(Icons.folder),
//tooltip: widget.tooltip,
)))),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.settingsThemeImages),
subtitle: Text(AppLocalizations.of(context)!.settingsThemeImagesDescription),
value: settings.themeImages,
onChanged: (bool value) {
settings.themeImages = value; // Save Settings...
saveSettings(context);
},
activeTrackColor: settings.theme.defaultButtonColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(Icons.image, color: settings.current().mainTextColor),
),
ListTile(
title: Text(AppLocalizations.of(context)!.settingUIColumnPortrait),
leading: Icon(Icons.table_chart, color: settings
.current()
.mainTextColor),
trailing: Container(
@ -435,4 +458,97 @@ class _GlobalSettingsAppearanceViewState extends State<GlobalSettingsAppearanceV
}
return theme;
}
void importThemeCheck(BuildContext context, Settings settings, String themesDir, String newThemeDirectory) async {
// check is theme
final srcDir = Directory(newThemeDirectory);
String themeName = path.basename(newThemeDirectory);
File themeFile = File(path.join(newThemeDirectory, "theme.yml"));
if (!themeFile.existsSync()) {
// error, isnt valid theme, no .yml theme file found
SnackBar err = SnackBar(content: Text(AppLocalizations.of(context)!.settingsThemeErrorInvalid.replaceAll("\$themeName", themeName)));
ScaffoldMessenger.of(context).showSnackBar(err);
return;
}
Directory targetDir = Directory(path.join(themesDir, themeName));
// check if exists
if (themes.containsKey(themeName) || targetDir.existsSync()) {
_modalConfirmOverwriteCustomTheme(srcDir, targetDir, themesDir, themeName, settings);
} else {
importTheme(srcDir, targetDir, themesDir, themeName, settings);
}
}
void importTheme(Directory srcDir, Directory targetDir, String themesDir, String themeName, Settings settings) async {
if (!targetDir.existsSync()) {
targetDir = await targetDir.create();
}
// importTheme(newVal)
await for (var entity in srcDir.list(recursive: false)) {
if (entity is File) {
entity.copySync(path.join(targetDir.path, path.basename(entity.path)));
}
}
var data = await loadFileYamlTheme(path.join(targetDir.path, "theme.yml"), targetDir.path);
if (data != null) {
themes[themeName] = data;
}
if (settings.current().theme == themeName) {
settings.setTheme(themeName, settings.current().mode);
}
}
void _modalConfirmOverwriteCustomTheme(Directory srcDir, Directory targetDir, String themesDir, String themeName, Settings settings) {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: RepaintBoundary(
child: Container(
height: Platform.isAndroid ? 250 : 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Column(mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: <Widget>[
Text(AppLocalizations.of(context)!.settingThemeOverwriteQuestion.replaceAll("\$themeName", themeName)),
SizedBox(
height: 20,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Spacer(),
Expanded(
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.settingThemeOverwriteConfirm, semanticsLabel: AppLocalizations.of(context)!.settingThemeOverwriteConfirm),
onPressed: () {
importTheme(srcDir, targetDir, themesDir, themeName, settings);
Navigator.pop(context);
},
)),
SizedBox(
width: 20,
),
Expanded(
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.cancel, semanticsLabel: AppLocalizations.of(context)!.cancel),
onPressed: () {
Navigator.pop(context);
},
)),
Spacer(),
],
)
]))))));
});
}
}

View File

@ -5,6 +5,7 @@ import 'dart:io';
import 'package:provider/provider.dart';
import '../settings.dart';
import 'buttontextfield.dart';
import 'package:path/path.dart' as path;
import 'cwtchlabel.dart';
class CwtchFolderPicker extends StatefulWidget {
@ -15,7 +16,8 @@ class CwtchFolderPicker extends StatefulWidget {
final Function(String)? onSave;
final Key? testKey;
final TextStyle? textStyle;
const CwtchFolderPicker({Key? key, this.testKey, this.textStyle, this.label = "", this.tooltip = "", this.initialValue = "", this.onSave, this.description = ""}) : super(key: key);
final IconData icon;
const CwtchFolderPicker({Key? key, this.testKey, this.textStyle, this.label = "", this.tooltip = "", this.initialValue = "", this.onSave, this.description = "", this.icon = Icons.file_download}) : super(key: key);
@override
_CwtchFolderPickerState createState() => _CwtchFolderPickerState();
@ -33,7 +35,7 @@ class _CwtchFolderPickerState extends State<CwtchFolderPicker> {
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(Icons.file_download, color: Provider.of<Settings>(context).theme.messageFromMeTextColor, size: 16),
leading: Icon(widget.icon, color: Provider.of<Settings>(context).theme.messageFromMeTextColor),
title: Text(widget.label),
subtitle: Text(widget.description),
trailing: Container(
@ -53,7 +55,7 @@ class _CwtchFolderPickerState extends State<CwtchFolderPicker> {
var selectedDirectory = await showSelectDirectoryPicker(context);
if (selectedDirectory != null) {
//File directory = File(selectedDirectory);
selectedDirectory += "/";
selectedDirectory += path.separator ;
ctrlrVal.text = selectedDirectory;
if (widget.onSave != null) {
widget.onSave!(selectedDirectory);