Compare commits

...

11 Commits

Author SHA1 Message Date
Lea Anthony
2e01710412 v2.0.0-alpha.63 2021-04-04 05:15:09 +10:00
Lea Anthony
1b0193161c Improvements for handling Info.plist 2021-04-04 05:14:16 +10:00
Lea Anthony
1b377fb575 Support enter as alias for return 2021-04-03 16:52:54 +11:00
Lea Anthony
1203ae64b8 v2.0.0-alpha.62 2021-03-31 15:44:28 +11:00
Lea Anthony
26ed8002b9 Menu bar font fix 2021-03-31 15:43:28 +11:00
Lea Anthony
cf23bffc67 v2.0.0-alpha.61 2021-03-30 22:22:48 +11:00
Lea Anthony
d70f6fffe7 Remove autorelease of colour. Fixes high sierra. 2021-03-30 22:21:49 +11:00
Lea Anthony
54c99fc386 Move some methods to main thread 2021-03-29 20:45:50 +11:00
Lea Anthony
86c1ea5e6a Initial support for compression 2021-03-27 20:59:14 +11:00
Lea Anthony
b394c1914c v2.0.0-alpha.60 2021-03-26 20:54:42 +11:00
Lea Anthony
91c2ddf155 Allow styling of menu items that have submenus 2021-03-26 20:54:19 +11:00
11 changed files with 100 additions and 310 deletions

View File

@@ -39,6 +39,9 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
compilerCommand := "go"
command.StringFlag("compiler", "Use a different go compiler to build, eg go1.15beta1", &compilerCommand)
compress := false
command.BoolFlag("compress", "Compress final binary", &compress)
// Setup Platform flag
platform := runtime.GOOS
command.StringFlag("platform", "Platform to target", &platform)
@@ -114,6 +117,10 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
return fmt.Errorf("platform %s is not supported", platform)
}
if compress && platform == "darwin/universal" {
println("Warning: compress flag unsupported for universal binaries. Ignoring.")
compress = false
}
// Create BuildOptions
buildOptions := &build.Options{
Logger: logger,
@@ -127,6 +134,7 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
KeepAssets: keepAssets,
AppleIdentity: appleIdentity,
Verbosity: verbosity,
Compress: compress,
}
// Calculate platform and arch
@@ -147,10 +155,12 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
}
// Write out the system information
fmt.Fprintf(w, "\n")
fmt.Fprintf(w, "App Type: \t%s\n", buildOptions.OutputType)
fmt.Fprintf(w, "Platform: \t%s\n", buildOptions.Platform)
fmt.Fprintf(w, "Arch: \t%s\n", buildOptions.Arch)
fmt.Fprintf(w, "Compiler: \t%s\n", buildOptions.Compiler)
fmt.Fprintf(w, "Compress: \t%t\n", buildOptions.Compress)
fmt.Fprintf(w, "Build Mode: \t%s\n", buildModeText)
fmt.Fprintf(w, "Package: \t%t\n", buildOptions.Pack)
fmt.Fprintf(w, "Clean Build Dir: \t%t\n", buildOptions.CleanBuildDirectory)

View File

@@ -1,3 +1,3 @@
package main
var version = "v2.0.0-alpha.59"
var version = "v2.0.0-alpha.63"

View File

@@ -1049,22 +1049,26 @@ void SetDebug(void *applicationPointer, int flag) {
void AddContextMenu(struct Application *app, const char *contextMenuJSON) {
// Guard against calling during shutdown
if( app->shuttingDown ) return;
AddContextMenuToStore(app->contextMenuStore, contextMenuJSON);
ON_MAIN_THREAD (
AddContextMenuToStore(app->contextMenuStore, contextMenuJSON);
);
}
void UpdateContextMenu(struct Application *app, const char* contextMenuJSON) {
// Guard against calling during shutdown
if( app->shuttingDown ) return;
UpdateContextMenuInStore(app->contextMenuStore, contextMenuJSON);
ON_MAIN_THREAD(
UpdateContextMenuInStore(app->contextMenuStore, contextMenuJSON);
);
}
void AddTrayMenu(struct Application *app, const char *trayMenuJSON) {
// Guard against calling during shutdown
if( app->shuttingDown ) return;
AddTrayMenuToStore(TrayMenuStoreSingleton, trayMenuJSON);
ON_MAIN_THREAD(
AddTrayMenuToStore(TrayMenuStoreSingleton, trayMenuJSON);
);
}
void SetTrayMenu(struct Application *app, const char* trayMenuJSON) {
@@ -1372,211 +1376,6 @@ void parseMenuRole(struct Application *app, id parentMenu, JsonNode *item) {
}
id parseTextMenuItem(struct Application *app, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char *menuCallback) {
id item = ALLOC("NSMenuItem");
id wrappedId = ((id(*)(id, SEL, const char*))objc_msgSend)(c("NSValue"), s("valueWithPointer:"), menuid);
msg_id(item, s("setRepresentedObject:"), wrappedId);
id key = processAcceleratorKey(acceleratorkey);
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), str(title),
s(menuCallback), key);
msg_bool(item, s("setEnabled:"), !disabled);
msg_reg(item, s("autorelease"));
// Process modifiers
if( modifiers != NULL ) {
unsigned long modifierFlags = parseModifiers(modifiers);
msg_int(item, s("setKeyEquivalentModifierMask:"), modifierFlags);
}
msg_id(parentMenu, s("addItem:"), item);
return item;
}
id parseCheckboxMenuItem(struct Application *app, id parentmenu, const char
*title, const char *menuid, bool disabled, bool checked, const char *key,
struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction) {
id item = ALLOC("NSMenuItem");
// Store the item in the menu item map
hashmap_put(menuItemMap, (char*)menuid, strlen(menuid), item);
id wrappedId = msg_id(c("NSValue"), s("valueWithPointer:"), (id)menuid);
msg_id(item, s("setRepresentedObject:"), wrappedId);
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), str(title), s(checkboxCallbackFunction), str(key));
msg_bool(item, s("setEnabled:"), !disabled);
msg_reg(item, s("autorelease"));
msg_int(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg_id(parentmenu, s("addItem:"), item);
return item;
}
id parseRadioMenuItem(struct Application *app, id parentmenu, const char *title,
const char *menuid, bool disabled, bool checked, const char *acceleratorkey,
struct hashmap_s *menuItemMap, const char *radioCallbackFunction) {
id item = ALLOC("NSMenuItem");
// Store the item in the menu item map
hashmap_put(menuItemMap, (char*)menuid, strlen(menuid), item);
id wrappedId = msg_id(c("NSValue"), s("valueWithPointer:"), (id)menuid);
msg_id(item, s("setRepresentedObject:"), wrappedId);
id key = processAcceleratorKey(acceleratorkey);
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), str(title), s(radioCallbackFunction), key);
msg_bool(item, s("setEnabled:"), !disabled);
msg_reg(item, s("autorelease"));
msg_int(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg_id(parentmenu, s("addItem:"), item);
return item;
}
void parseMenuItem(struct Application *app, id parentMenu, JsonNode *item,
struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char
*radioCallbackFunction, const char *menuCallbackFunction) {
// Check if this item is hidden and if so, exit early!
bool hidden = false;
getJSONBool(item, "Hidden", &hidden);
if( hidden ) {
return;
}
// Get the role
JsonNode *role = json_find_member(item, "Role");
if( role != NULL ) {
parseMenuRole(app, parentMenu, role);
return;
}
// Check if this is a submenu
JsonNode *submenu = json_find_member(item, "SubMenu");
if( submenu != NULL ) {
// Get the label
JsonNode *menuNameNode = json_find_member(item, "Label");
const char *name = "";
if ( menuNameNode != NULL) {
name = menuNameNode->string_;
}
id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
id thisMenu = createMenu(str(name));
msg_id(thisMenuItem, s("setSubmenu:"), thisMenu);
msg_id(parentMenu, s("addItem:"), thisMenuItem);
// Loop over submenu items
JsonNode *item;
json_foreach(item, submenu) {
// Get item label
parseMenuItem(app, thisMenu, item, menuItemMap, checkboxCallbackFunction, radioCallbackFunction, menuCallbackFunction);
}
return;
}
// This is a user menu. Get the common data
// Get the label
const char *label = getJSONString(item, "Label");
if ( label == NULL) {
label = "(empty)";
}
const char *menuid = getJSONString(item, "ID");
if ( menuid == NULL) {
menuid = "";
}
bool disabled = false;
getJSONBool(item, "Disabled", &disabled);
// Get the Accelerator
JsonNode *accelerator = json_find_member(item, "Accelerator");
const char *acceleratorkey = NULL;
const char **modifiers = NULL;
// If we have an accelerator
if( accelerator != NULL ) {
// Get the key
acceleratorkey = getJSONString(accelerator, "Key");
// Check if there are modifiers
JsonNode *modifiersList = json_find_member(accelerator, "Modifiers");
if ( modifiersList != NULL ) {
// Allocate an array of strings
int noOfModifiers = json_array_length(modifiersList);
// Do we have any?
if (noOfModifiers > 0) {
modifiers = malloc(sizeof(const char *) * (noOfModifiers + 1));
JsonNode *modifier;
int count = 0;
// Iterate the modifiers and save a reference to them in our new array
json_foreach(modifier, modifiersList) {
// Get modifier name
modifiers[count] = modifier->string_;
count++;
}
// Null terminate the modifier list
modifiers[count] = NULL;
}
}
}
// Get the Type
JsonNode *type = json_find_member(item, "Type");
if( type != NULL ) {
if( STREQ(type->string_, "Text")) {
parseTextMenuItem(app, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, menuCallbackFunction);
}
else if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu);
}
else if ( STREQ(type->string_, "Checkbox")) {
// Get checked state
bool checked = false;
getJSONBool(item, "Checked", &checked);
parseCheckboxMenuItem(app, parentMenu, label, menuid, disabled, checked, "", menuItemMap, checkboxCallbackFunction);
}
else if ( STREQ(type->string_, "Radio")) {
// Get checked state
bool checked = false;
getJSONBool(item, "Checked", &checked);
parseRadioMenuItem(app, parentMenu, label, menuid, disabled, checked, "", menuItemMap, radioCallbackFunction);
}
if ( modifiers != NULL ) {
free(modifiers);
}
return;
}
}
void parseMenu(struct Application *app, id parentMenu, JsonNode *menu, struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char *radioCallbackFunction, const char *menuCallbackFunction) {
JsonNode *items = json_find_member(menu, "Items");
if( items == NULL ) {
// Parse error!
Fatal(app, "Unable to find Items in Menu");
return;
}
// Iterate items
JsonNode *item;
json_foreach(item, items) {
// Get item label
parseMenuItem(app, parentMenu, item, menuItemMap, checkboxCallbackFunction, radioCallbackFunction, menuCallbackFunction);
}
}
void dumpMemberList(const char *name, id *memberList) {
void *member = memberList[0];
int count = 0;
@@ -1589,43 +1388,6 @@ void dumpMemberList(const char *name, id *memberList) {
printf("]\n");
}
void processRadioGroup(JsonNode *radioGroup, struct hashmap_s *menuItemMap,
struct hashmap_s *radioGroupMap) {
int groupLength;
getJSONInt(radioGroup, "Length", &groupLength);
JsonNode *members = json_find_member(radioGroup, "Members");
JsonNode *member;
// Allocate array
size_t arrayLength = sizeof(id)*(groupLength+1);
id memberList[arrayLength];
// Build the radio group items
int count=0;
json_foreach(member, members) {
// Get menu by id
id menuItem = (id)hashmap_get(menuItemMap, (char*)member->string_, strlen(member->string_));
// Save Member
memberList[count] = menuItem;
count = count + 1;
}
// Null terminate array
memberList[groupLength] = 0;
// dumpMemberList("memberList", memberList);
// Store the members
json_foreach(member, members) {
// Copy the memberList
char *newMemberList = (char *)malloc(arrayLength);
memcpy(newMemberList, memberList, arrayLength);
// add group to each member of group
hashmap_put(radioGroupMap, member->string_, strlen(member->string_), newMemberList);
}
}
// updateMenu replaces the current menu with the given one
void updateMenu(struct Application *app, const char *menuAsJSON) {
Debug(app, "Menu is now: %s", menuAsJSON);
@@ -1649,7 +1411,9 @@ void SetApplicationMenu(struct Application *app, const char *menuAsJSON) {
}
// Update menu
updateMenu(app, menuAsJSON);
ON_MAIN_THREAD (
updateMenu(app, menuAsJSON);
);
}
void processDialogIcons(struct hashmap_s *hashmap, const unsigned char *dialogIcons[]) {

View File

@@ -189,6 +189,9 @@ id processAcceleratorKey(const char *key) {
if( STREQ(key, "return") ) {
return strunicode(0x000d);
}
if( STREQ(key, "enter") ) {
return strunicode(0x000d);
}
if( STREQ(key, "escape") ) {
return strunicode(0x001b);
}
@@ -578,30 +581,26 @@ id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const c
id createAttributedString(const char* title, const char* fontName, int fontSize, const char* RGBA) {
// Process Menu Item attributes
// Create new Dictionary
id dictionary = ALLOC_INIT("NSMutableDictionary");
// Process font
CGFloat fontSizeFloat = (CGFloat)fontSize;
// Check if valid
id fontNameAsNSString = str(fontName);
id font = ((id(*)(id, SEL, id, CGFloat))objc_msgSend)(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat);
if( font == NULL ) {
bool supportsMonospacedDigitSystemFont = (bool) ((id(*)(id, SEL, SEL))objc_msgSend)(c("NSFont"), s("respondsToSelector:"), s("monospacedDigitSystemFontOfSize:weight:"));
if( supportsMonospacedDigitSystemFont ) {
font = ((id(*)(id, SEL, CGFloat, CGFloat))objc_msgSend)(c("NSFont"), s("monospacedDigitSystemFontOfSize:weight:"), fontSizeFloat, (CGFloat)NSFontWeightRegular);
} else {
font = ((id(*)(id, SEL, CGFloat))objc_msgSend)(c("NSFont"), s("menuFontOfSize:"), fontSizeFloat);
// Use default font
id font = ((id(*)(id, SEL, CGFloat))objc_msgSend)(c("NSFont"), s("menuBarFontOfSize:"), fontSizeFloat);
// Check user supplied font
if( STR_HAS_CHARS(fontName) ) {
id fontNameAsNSString = str(fontName);
id userFont = ((id(*)(id, SEL, id, CGFloat))objc_msgSend)(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat);
if( userFont != NULL ) {
font = userFont;
}
}
// Add font to dictionary
id fan = lookupStringConstant(str("NSFontAttributeName"));
msg_id_id(dictionary, s("setObject:forKey:"), font, fan);
id offset = msg_float(c("NSNumber"), s("numberWithFloat:"), (float)0.0);
id offsetAttrName = lookupStringConstant(str("NSBaselineOffsetAttributeName"));
msg_id_id(dictionary, s("setObject:forKey:"), offset, offsetAttrName);
// RGBA
if( RGBA != NULL && strlen(RGBA) > 0) {
unsigned short r, g, b, a;
@@ -617,14 +616,13 @@ id createAttributedString(const char* title, const char* fontName, int fontSize,
(CGFloat)a / (CGFloat)255.0);
id NSForegroundColorAttributeName = lookupStringConstant(str("NSForegroundColorAttributeName"));
msg_id_id(dictionary, s("setObject:forKey:"), colour, NSForegroundColorAttributeName);
msg_reg(colour, s("autorelease"));
}
}
id attributedString = ALLOC("NSMutableAttributedString");
msg_id_id(attributedString, s("initWithString:attributes:"), str(title), dictionary);
msg_reg(attributedString, s("autorelease"));
msg_reg(dictionary, s("release"));
msg_reg(dictionary, s("autorelease"));
return attributedString;
}
@@ -695,38 +693,6 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
return;
}
// Check if this is a submenu
JsonNode *submenu = json_find_member(item, "SubMenu");
if( submenu != NULL ) {
// Get the label
JsonNode *menuNameNode = json_find_member(item, "Label");
const char *name = "";
if ( menuNameNode != NULL) {
name = menuNameNode->string_;
}
id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
id thisMenu = createMenu(str(name));
msg_id(thisMenuItem, s("setSubmenu:"), thisMenu);
msg_id(parentMenu, s("addItem:"), thisMenuItem);
JsonNode *submenuItems = json_find_member(submenu, "Items");
// If we have no items, just return
if ( submenuItems == NULL ) {
return;
}
// Loop over submenu items
JsonNode *item;
json_foreach(item, submenuItems) {
// Get item label
processMenuItem(menu, thisMenu, item);
}
return;
}
// This is a user menu. Get the common data
// Get the label
const char *label = getJSONString(item, "Label");
@@ -792,9 +758,36 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
// Get the Type
JsonNode *type = json_find_member(item, "Type");
if( type != NULL ) {
if( STREQ(type->string_, "Text") || STREQ(type->string_, "Submenu")) {
id thisMenuItem = processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA, templateImage, alternate);
if( STREQ(type->string_, "Text")) {
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA, templateImage, alternate);
// Check if this node has a submenu
JsonNode *submenu = json_find_member(item, "SubMenu");
if( submenu != NULL ) {
// Get the label
JsonNode *menuNameNode = json_find_member(item, "Label");
const char *name = "";
if ( menuNameNode != NULL) {
name = menuNameNode->string_;
}
id thisMenu = createMenu(str(name));
msg_id(thisMenuItem, s("setSubmenu:"), thisMenu);
JsonNode *submenuItems = json_find_member(submenu, "Items");
// If we have no items, just return
if ( submenuItems == NULL ) {
return;
}
// Loop over submenu items
JsonNode *item;
json_foreach(item, submenuItems) {
// Get item label
processMenuItem(menu, thisMenu, item);
}
}
}
else if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu);
@@ -813,7 +806,6 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
processRadioMenuItem(menu, parentMenu, label, menuid, disabled, checked, "");
}
}
if ( modifiers != NULL ) {

View File

@@ -10,6 +10,8 @@ import (
"runtime"
"strings"
"github.com/pkg/errors"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/assetdb"
"github.com/wailsapp/wails/v2/internal/fs"
@@ -291,6 +293,27 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
return fmt.Errorf("%s\n%s", err, string(stde.Bytes()))
}
if !options.Compress {
return nil
}
// Do we have upx installed?
if !shell.CommandExists("upx") {
println("Warning: Cannot compress binary: upx not found")
return nil
}
if verbose {
println(" Compressing with:", "upx", "--best", "--no-color", "--no-progress", options.CompiledBinary)
}
output, err := exec.Command(options.BuildDirectory, "upx", "--best", "--no-color", "--no-progress", options.CompiledBinary).Output()
if err != nil {
return errors.Wrap(err, "Error during compression:")
}
if verbose {
println(output)
}
return nil
}

View File

@@ -42,8 +42,9 @@ type Options struct {
BuildDirectory string // Directory to use for building the application
CleanBuildDirectory bool // Indicates if the build directory should be cleaned before building
CompiledBinary string // Fully qualified path to the compiled binary
KeepAssets bool // /Keep the generated assets/files
KeepAssets bool // Keep the generated assets/files
Verbosity int // Verbosity level (0 - silent, 1 - default, 2 - verbose)
Compress bool // Compress the final binary
AppleIdentity string
}

View File

@@ -49,4 +49,4 @@ Example:
## Mac
The `mac` directory holds files specific to Mac builds, such as `info.plist`. These may be edited and used as part of the build.
The `mac` directory holds files specific to Mac builds, such as `Info.plist`. These may be edited and used as part of the build.

View File

@@ -42,7 +42,7 @@ func packageApplication(options *Options) error {
return errors.Wrap(err, "Cannot move file: "+options.ProjectData.OutputFilename)
}
// Generate info.plist
// Generate Info.plist
err = processPList(options, contentsDirectory)
if err != nil {
return err
@@ -68,7 +68,7 @@ func packageApplication(options *Options) error {
func processPList(options *Options, contentsDirectory string) error {
// Check if plist already exists in project dir
plistFile := filepath.Join(options.ProjectData.AssetsDir, "mac", "info.plist")
plistFile := filepath.Join(options.ProjectData.AssetsDir, "mac", "Info.plist")
// If the file doesn't exist, generate it
if !fs.FileExists(plistFile) {
@@ -79,7 +79,7 @@ func processPList(options *Options, contentsDirectory string) error {
}
// Copy it to the contents directory
targetFile := filepath.Join(contentsDirectory, "info.plist")
targetFile := filepath.Join(contentsDirectory, "Info.plist")
return fs.CopyFile(plistFile, targetFile)
}
@@ -88,11 +88,11 @@ func generateDefaultPlist(options *Options, targetPlistFile string) error {
exe := defaultString(options.OutputFile, name)
version := "1.0.0"
author := defaultString(options.ProjectData.Author.Name, "Anonymous")
packageID := strings.Join([]string{"wails", name, version}, ".")
packageID := strings.Join([]string{"wails", name}, ".")
plistData := newPlistData(name, exe, packageID, version, author)
tmpl := template.New("infoPlist")
plistTemplate := fs.RelativePath("./internal/packager/darwin/info.plist")
plistTemplate := fs.RelativePath("./internal/packager/darwin/Info.plist")
infoPlist, err := ioutil.ReadFile(plistTemplate)
if err != nil {
return errors.Wrap(err, "Cannot open plist template")

View File

@@ -8,7 +8,7 @@ import (
"github.com/leaanthony/slicer"
)
var namedKeys = slicer.String([]string{"backspace", "tab", "return", "escape", "left", "right", "up", "down", "space", "delete", "home", "end", "page up", "page down", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "f21", "f22", "f23", "f24", "f25", "f26", "f27", "f28", "f29", "f30", "f31", "f32", "f33", "f34", "f35", "numlock"})
var namedKeys = slicer.String([]string{"backspace", "tab", "return", "enter", "escape", "left", "right", "up", "down", "space", "delete", "home", "end", "page up", "page down", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "f21", "f22", "f23", "f24", "f25", "f26", "f27", "f28", "f29", "f30", "f31", "f32", "f33", "f34", "f35", "numlock"})
func parseKey(key string) (string, bool) {

View File

@@ -1 +1 @@
info.plist
Info.plist