Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1910363
implemented an absolute arrangement component and add x and y coordin…
ObadaAlkhatib Aug 23, 2019
e7a4251
minor fix
ObadaAL Aug 23, 2019
c298661
fixed indentation and added copyright header
ObadaAL Aug 29, 2019
65fc938
Merge branch 'ucr' into HEAD
ewpatton May 14, 2021
5571773
Address code style issues
ewpatton May 14, 2021
d1fba1d
Address GWT deprecations
ewpatton May 14, 2021
6f6e635
Make position updatable via blocks
ewpatton May 18, 2021
0fefe2c
Bump YOUNG_ANDROID_VERSION for merge
ewpatton May 19, 2021
8a9ab8d
Merge remote-tracking branch 'github/ucr' into HEAD
ewpatton Apr 3, 2024
1fa732f
Merge remote-tracking branch 'github/ucr' into HEAD
ewpatton Feb 19, 2025
2a0b278
updates to readme
Feb 20, 2025
a75e5ed
first fix to #3386
Feb 21, 2025
973a9b9
update, working in layout but not programmatically
Feb 25, 2025
1930525
#3386 working programmatically with view hiearchy
Feb 25, 2025
d701cb4
Merge pull request #1 from cindyloo/cindy-absolute-arrangement-ios
cindyloo May 5, 2025
9b39100
Make Blockly controls high contrast with background again
ewpatton Feb 19, 2025
681a9ae
Update AIProject.java (#3303)
YashSachdeva369 Feb 21, 2025
3d27ecb
Fixed passwordtextbox textchanged function not working previously
ghu999 Jan 30, 2025
4048b2d
Add @UsesXmls annotation to create xml files in APK resources (#3292)
patryk84a Feb 21, 2025
c6f57d7
Add clearer error messages when sending to gallery (#2791)
arinmodi Feb 24, 2025
3710890
Fix internal error from dictionaries drawer in Japanese
ewpatton Feb 19, 2025
f236fb8
Add Support for conducting surveys of users
jisqyv Feb 13, 2025
19aeba6
Refactor trash management (#3342)
SusanRatiLane Feb 26, 2025
b35f301
Adapting Mock ListView to new ListView properties. (#3280)
patryk84a Feb 26, 2025
f57eb8c
Fix expired service overlay for Neo
ewpatton Feb 27, 2025
4fac821
Adding some empty properties and AddItems(AtIndex)
patryk84a Mar 1, 2025
4220f95
implement elementColor, dividerColor (singleLine) , cornerRadius
Mar 3, 2025
8ea20a5
remove commented out code
Mar 6, 2025
4fc85b5
tweak to element background handling
Mar 6, 2025
3304ed6
update to ListView for elementColor
Mar 7, 2025
5cdb143
add ElementColor with None type to assert backgroundColor in cell.Bac…
Mar 10, 2025
aed9ada
more tests
Mar 10, 2025
757e892
cornerradius: divide by a constant to ensure similar look w Android. …
Nov 5, 2025
2c6df73
Horizontal view is working now (thanks vibe coding)
Nov 5, 2025
9f05c8f
collectionView properly persisted, toggle works
Nov 5, 2025
218dc2b
collectionView properly persisted, toggle works
Nov 5, 2025
c741944
New block for Notifier - can display image (local url) along with tex…
Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ ant -Dskip.ios=true

iOS builds will automatically be skipped on other operating systems.

## Setup Instructions (iOS Support)

We generally use Xcode for iOS development. Open the
AppInventor.xcworkspace file to view the Xcode workspace. This
workspace includes three projects:
Expand All @@ -125,12 +127,14 @@ workspace includes three projects:
* AICompanionApp: The App Inventor companion written in Swift.

In Xcode you can run the AICompanionApp on your device by selecting
the AICompanionApp target's Debug scheme and pressing the Run button.
the AICompanionApp target's Debug scheme (top of window) and pressing the Run button.



For more information about iOS support, please see
For more information about iOS support and running from the command line (vs within XCode), please see
[README.ios.md](README.ios.md).

## Setup Instructions (iOS Support)


Building MIT App Inventor Companion for iOS requires an Apple
Macintosh computer running macOS 12 or later with Xcode 14 or later
Expand All @@ -141,18 +145,21 @@ added the relevant mobile provisioning profiles from the Developer
portal to your Xcode organizer (see Apple's website on instructions on
how to do this).

To build the MIT App Inventor companion, you will need to create a
file called AICompanionApp.xcconfig in the components-ios directory
To build the MIT App Inventor companion, you will need to copy the `AICompanionApp.xcconfig.sample` a
file called `AICompanionApp.xcconfig` in the components-ios directory
that sets your development team. The easiest way to do this is to copy
the AICompanionApp.xcconfig.sample file and edit it. Alternatively,
create a file with the following line:



```conf
DEVELOPMENT_TEAM = ID
DEVELOPMENT_TEAM = #ID
BUNDLE_IDENTIFIER = edu.mit.appinventor.aicompanion3
```

where ID is the development team ID shown in the Apple Developer
Portal. This ID is unique to your developer account (individual or
where ID is the development team ID shown in the *Apple Developer
Portal.* This ID is unique to your developer account (individual or
organization).


Expand Down
8 changes: 8 additions & 0 deletions appinventor/AIComponentKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@
4656A6452A6076090097DF06 /* CircularProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4656A6442A6076090097DF06 /* CircularProgress.swift */; };
46E77D8E29DD6894002CCDFB /* ShowAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46E77D8D29DD6894002CCDFB /* ShowAlert.swift */; };
5132E481C0B949ECDE06AE78 /* Pods_AIComponentKit_AIComponentKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4FD2633D7266389EB43FF45F /* Pods_AIComponentKit_AIComponentKitTests.framework */; };
5DCF6AE22D6905EE00410BBF /* AbsoluteArrangement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DCF6AE12D6903D900410BBF /* AbsoluteArrangement.swift */; };
5DCF6AE42D690E2F00410BBF /* RelativeLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DCF6AE32D690E2600410BBF /* RelativeLayout.swift */; };
5F7ED1062C35C950004418C5 /* tf-converter-1.7.4.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 5F7ED1002C35C94C004418C5 /* tf-converter-1.7.4.min.js */; };
5F7ED1072C35C950004418C5 /* facemesh.js in Resources */ = {isa = PBXBuildFile; fileRef = 5F7ED1012C35C94D004418C5 /* facemesh.js */; };
5F7ED1082C35C950004418C5 /* facemesh.html in Resources */ = {isa = PBXBuildFile; fileRef = 5F7ED1022C35C94E004418C5 /* facemesh.html */; };
Expand Down Expand Up @@ -703,6 +705,8 @@
46E77D8D29DD6894002CCDFB /* ShowAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowAlert.swift; sourceTree = "<group>"; };
4FD2633D7266389EB43FF45F /* Pods_AIComponentKit_AIComponentKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AIComponentKit_AIComponentKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
58E89B4AF39EEFF895CBBDD1 /* Pods-AIComponentKit-AIComponentKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AIComponentKit-AIComponentKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AIComponentKit-AIComponentKitTests/Pods-AIComponentKit-AIComponentKitTests.release.xcconfig"; sourceTree = "<group>"; };
5DCF6AE12D6903D900410BBF /* AbsoluteArrangement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsoluteArrangement.swift; sourceTree = "<group>"; };
5DCF6AE32D690E2600410BBF /* RelativeLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelativeLayout.swift; sourceTree = "<group>"; };
5F7ED1002C35C94C004418C5 /* tf-converter-1.7.4.min.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "tf-converter-1.7.4.min.js"; sourceTree = "<group>"; };
5F7ED1012C35C94D004418C5 /* facemesh.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = facemesh.js; sourceTree = "<group>"; };
5F7ED1022C35C94E004418C5 /* facemesh.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = facemesh.html; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1131,8 +1135,10 @@
isa = PBXGroup;
children = (
06B215F91DB950F6000B3366 /* HorizontalArrangement.swift */,
5DCF6AE12D6903D900410BBF /* AbsoluteArrangement.swift */,
06D151E91DC6ED5200FC981E /* HVArrangement.swift */,
06B215FB1DB95109000B3366 /* VerticalArrangement.swift */,
5DCF6AE32D690E2600410BBF /* RelativeLayout.swift */,
0680ED581E012A2100732883 /* TableArrangement.swift */,
C4D38CC6230DCDF300D09FE3 /* VerticalScrollArrangement.swift */,
C4D38CC8230DCF9400D09FE3 /* HorizontalScrollArrangement.swift */,
Expand Down Expand Up @@ -1951,6 +1957,7 @@
06D998EE1F53CDDB00A13A0E /* ContactPicker.swift in Sources */,
461809A12A671463001854C4 /* ProximitySensor.swift in Sources */,
DE1443D820C842EE00A250F1 /* Pedometer.swift in Sources */,
5DCF6AE42D690E2F00410BBF /* RelativeLayout.swift in Sources */,
068F5D7E2B8D16EA00A24EB0 /* PieChartView.swift in Sources */,
06C791D12950E94500C1BB1B /* ChartView.swift in Sources */,
06E59EB51D93345E00C42804 /* ButtonBase.swift in Sources */,
Expand Down Expand Up @@ -2054,6 +2061,7 @@
065E429D2866BC5500479153 /* Navigation.swift in Sources */,
06E91E122D2B236800377845 /* LOBFValues.swift in Sources */,
DED39C2F2005537D0054B646 /* Camcorder.swift in Sources */,
5DCF6AE22D6905EE00410BBF /* AbsoluteArrangement.swift in Sources */,
062217F02A37767F00B89562 /* image.pb.swift in Sources */,
06C70BB52A8BCF9D00C6C78D /* OAuth2CallbackHelper.m in Sources */,
DE91AB931F93FF8F00AB742A /* PhoneNumberPicker.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,142 +13,148 @@

/**
* A representation of an App Inventor project. This includes its path, name,
* list of assets, list of screens and project properties file.
*
* list of assets, list of screens, and project properties file.
*
* <p>Provides functionality to validate and extract components of the project.
*
* @author [email protected] (Kate Feeney)
*/
public class AIProject {

// Backing for the project's directory path from home
private String projectPath;
/** Path to the project file. */
private final String projectPath;

// Backing for the list of AIScreens
private List<AIScreen> screensList;
/** List of screens in the project. */
private final List<AIScreen> screensList;

// Backing for the list of AIAssets
private List<AIAsset> assetsList;
/** List of assets in the project. */
private final List<AIAsset> assetsList;

// Backing for the path to the projects properties file
/** Path to the project's properties file. */
private String propertiesFilePath;

// Backing for if the project is a valid source file
/** Indicates whether the project is valid. */
private boolean valid;

/**
* Creates a new AIProject.
*
* @param projectPath the path to the project from the home directory
*
* @param projectPath the path to the project file (ZIP archive)
*/
public AIProject(String projectPath) {
try {
this.projectPath = projectPath;
// Create screens list.
this.screensList = new LinkedList<AIScreen>();
// Create assets list.
this.assetsList = new LinkedList<AIAsset>();
// Go through each file in the project and create the appropriate classes.
Enumeration<? extends ZipEntry> e = new ZipFile(new File(projectPath)).entries();
while (e.hasMoreElements()) {
// fileName is the path of the file in the project file.
String fileName = (new ZipEntry(e.nextElement())).getName();
// Create an AIScreen from any screen file in the project's src folder.
this.projectPath = projectPath;
this.screensList = new LinkedList<>();
this.assetsList = new LinkedList<>();
this.valid = false;
initializeProject();
}

/**
* Initializes the project by reading the ZIP file and extracting components.
*/
private void initializeProject() {
try (ZipFile zipFile = new ZipFile(new File(projectPath))) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();

while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String fileName = entry.getName();

if (fileName.startsWith("src") && fileName.endsWith(".scm")) {
AIScreen screen = new AIScreen(fileName);
screensList.add(screen);
// Create an AIAsset from any file in the project's assets folder.
screensList.add(new AIScreen(fileName));
} else if (fileName.startsWith("assets")) {
AIAsset asset = new AIAsset(fileName);
assetsList.add(asset);
assetsList.add(new AIAsset(fileName));
} else if (fileName.endsWith("project.properties")) {
this.setPropertiesFilePath(fileName);
propertiesFilePath = fileName;
}
}
// Check if valid project, if not show error.
valid = screensList != null && propertiesFilePath != null;
if (!valid) {
JOptionPane.showMessageDialog(AIMerger.getInstance().myCP, "The selected project is not a"
+ " project source file! Project source files are zip files.", "File error",
JOptionPane.ERROR_MESSAGE);
}

validateProject();
} catch (ZipException e) {
JOptionPane.showMessageDialog(AIMerger.getInstance().myCP,
"The selected project is not a project source file! Project source files are zip files.",
"File error", JOptionPane.ERROR_MESSAGE);
valid = false;
showErrorDialog("The selected file is not a valid project source file. Source files must be ZIP archives.");
} catch (IOException e) {
JOptionPane.showMessageDialog(AIMerger.getInstance().myCP,
"The selected project is not a project source file! Project source files are zip files.",
"File error", JOptionPane.ERROR_MESSAGE);
valid = false;
showErrorDialog("An error occurred while reading the project file.");
}
}

/**
* Returns the AIProject's name.
*
* @return AIProject's name
* Validates the project by ensuring essential components are present.
*/
public String getProjectName() {
// The projectName is the name of the zip file.
if (projectPath.contains(File.separator)) {
return projectPath.substring(projectPath.lastIndexOf(File.separator) + 1,
projectPath.lastIndexOf("."));
} else {
return projectPath;
private void validateProject() {
valid = !screensList.isEmpty() && propertiesFilePath != null;
if (!valid) {
showErrorDialog("The selected project is missing essential components (e.g., screens or properties file).");
}
}

/**
* Returns the AIProject's path from home directory.
*
* @return AIProject's path from home directory
* Displays an error dialog with the given message.
*
* @param message the error message to display
*/
private void showErrorDialog(String message) {
JOptionPane.showMessageDialog(
AIMerger.getInstance().myCP,
message,
"File Error",
JOptionPane.ERROR_MESSAGE
);
}

/**
* Returns the name of the project, derived from the ZIP file name.
*
* @return the project name
*/
public String getProjectName() {
File file = new File(projectPath);
String fileName = file.getName();
int dotIndex = fileName.lastIndexOf('.');
return dotIndex > 0 ? fileName.substring(0, dotIndex) : fileName;
}

/**
* Returns the path to the project file.
*
* @return the project file path
*/
public String getProjectPath() {
return projectPath;
}

/**
* Returns the AIProject's list of AIScreens.
*
* @return list of project's AIScreens
* Returns the list of screens in the project.
*
* @return a list of {@link AIScreen} objects
*/
public List<AIScreen> getScreensList() {
return screensList;
}

/**
* Returns the AIProject's list of AIAssets.
*
* @return list of project's AIAssets
* Returns the list of assets in the project.
*
* @return a list of {@link AIAsset} objects
*/
public List<AIAsset> getAssetsList() {
return assetsList;
}

/**
* Returns the path to the projects properties file from project file.
*
* @return path to project's properties file within the project file
* Returns the path to the project's properties file within the ZIP archive.
*
* @return the properties file path
*/
public String getPropertiesFilePath() {
return propertiesFilePath;
}

/**
* Sets the projects properties file
*
* @param propertiesFilePath the path to the project's properties file within the project
*/
public void setPropertiesFilePath(String propertiesFilePath) {
this.propertiesFilePath = propertiesFilePath;
}

/**
* Returns if the project is valid and can be used for a merge.
*
* @return if project is valid
* Checks whether the project is valid for merging.
*
* @return {@code true} if the project is valid, {@code false} otherwise
*/
public boolean isValid() {
return this.valid;
return valid;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,12 @@ public void onUncaughtException(Throwable e) {
c -> userInfoService.getSystemConfig(sessionId, c))
.then(result -> {
config = result;
// Before we get too far into it, let's see if we have to do a survey!
String surveyUrl = config.getSurveyUrl();
if (surveyUrl != null && !surveyUrl.isEmpty()) {
Window.Location.replace(Urls.makeUri(surveyUrl, true));
// off we go, no returning
}
user = result.getUser();
isReadOnly = user.isReadOnly();
registerIosExtensions(config.getIosExtensions());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ public interface OdeMessages extends Messages, ComponentTranslations {
@Description("Error given when sending fails")
String GallerySendingError();

@DefaultMessage("This project contains extensions and cannot be published to gallery.")
@Description("Error Message for displaying error when user tries to publish the project" +
"containing extensions")
String ProjectContainsExtensions();

@DefaultMessage("Error Logging Into the Gallery")
@Description("Error given if login fails for some reason")
String GalleryLoginError();
Expand Down Expand Up @@ -1820,13 +1825,20 @@ public interface OdeMessages extends Messages, ComponentTranslations {
@Description("Confirmation message for selecting multiple projects and clicking delete")
String confirmDeleteManyProjects(String projectNames);

@DefaultMessage("Are you sure you want to move these projects to trash: {0}?")
@DefaultMessage("Are you sure you want to move these items to trash? {0}")
@Description("Confirmation message for selecting multiple projects and clicking trash")
String confirmMoveToTrash(String projectNames);
@DefaultMessage("Are you sure you want to delete these items permanently? {0}")
@Description("Confirmation message for selecting multiple projects in trash and clicking delete")
String confirmDeleteForever(String projectNames);

@DefaultMessage("Projects: {0}")
@Description("Information on selected projects for delete/trash confirmation")
String confirmTrashDeleteProjects(String projectNames);

@DefaultMessage("Are you sure you want to move {0} projects to trash?")
@Description("Confirmation message deleting large number of selected projects")
String confirmMoveToTrashCount(String projectNames);
@DefaultMessage("Folders: {0}")
@Description("Information on selected folders for delete/trash confirmation")
String confirmTrashDeleteFolders(String folderNames);

@DefaultMessage("Server error: could not delete project. Please try again later!")
@Description("Error message reported when deleting a project failed on the server.")
Expand Down
Loading