├── .gitattributes
├── .github
└── sasha-logo.png
├── .gitignore
├── .sasha
├── file_templates
│ ├── android-template.sketch
│ └── ios-template.sketch
└── project.sasha
├── .swiftlint.yml
├── .travis.yml
├── Config.xcconfig
├── LICENSE
├── Makefile
├── Package.pins
├── Package.resolved
├── Package.swift
├── README.md
└── Sources
├── Sasha
└── main.swift
└── SashaCore
├── Commands
├── Command.swift
├── CommandRegistry.swift
├── IconsCommand.swift
└── ProjectCommand.swift
├── Models
├── AndroidIcon.swift
├── AppIconSet.swift
├── Asset.swift
├── ComplicationSet.swift
├── Icon.swift
├── IconFactory.swift
├── IconRepresentable.swift
├── Info.swift
└── Platform.swift
├── Services
├── FolderService.swift
└── IconService.swift
└── Tasks
├── IconsTask.swift
└── ProjectTask.swift
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.yml linguist-generated
2 | *.sh linguist-generated
3 | *.podspec linguist-generated
4 | Makefile linguist-generated
5 |
6 | *.md linguist-documentation
7 |
--------------------------------------------------------------------------------
/.github/sasha-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemnovichkov/sasha/593e87a837aef61e7e86e12ab980fb16397a1009/.github/sasha-logo.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 |
--------------------------------------------------------------------------------
/.sasha/file_templates/android-template.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemnovichkov/sasha/593e87a837aef61e7e86e12ab980fb16397a1009/.sasha/file_templates/android-template.sketch
--------------------------------------------------------------------------------
/.sasha/file_templates/ios-template.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemnovichkov/sasha/593e87a837aef61e7e86e12ab980fb16397a1009/.sasha/file_templates/ios-template.sketch
--------------------------------------------------------------------------------
/.sasha/project.sasha:
--------------------------------------------------------------------------------
1 | iOS
2 | -UI
3 | --old
4 | --png
5 | -UX
6 | --old
7 | --png
8 | Android
9 | -UI
10 | --old
11 | --png
12 | -UX
13 | --old
14 | --png
15 | references
16 | -main_screens
17 | -menu
18 | -cards
19 | -another_case
20 | stuff
21 | -logos
22 | -icons
23 | -patterns
24 | -stocks
25 | -source
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | # paths to ignore during linting. Takes precedence over `included`.
2 |
3 | excluded:
4 | - .build/
5 |
6 | # Rules
7 |
8 | opt_in_rules:
9 | - conditional_returns_on_newline
10 | - fatal_error_message
11 | - overridden_super_call
12 |
13 | force_cast: error
14 | force_try: error
15 |
16 | colon:
17 | apply_to_dictionaries: false
18 |
19 | identifier_name:
20 | excluded:
21 | - id
22 | - URL
23 |
24 | statement_position:
25 | statement_mode: uncuddled_else
26 |
27 | # Constants
28 |
29 | file_length:
30 | warning: 1000
31 | error: 1500
32 |
33 | type_body_length:
34 | warning: 800
35 | error: 1000
36 |
37 | line_length: 140
38 |
39 | function_parameter_count: 4
40 |
41 | type_name:
42 | min_length: 2 # only warning
43 |
44 | # Custom rules
45 |
46 | custom_rules:
47 | accessors_and_observers_on_newline:
48 | name: "Property accessors and observers on newline"
49 | regex: "^\\s*(get|set|didSet|willSet)[\\h\\S]*\\{[\\h\\S]+$"
50 | message: "Property accessors and observers should always start with a newline."
51 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | os: osx
2 | language: swift
3 | osx_image: xcode10
4 | script:
5 | - make build
6 |
7 |
--------------------------------------------------------------------------------
/Config.xcconfig:
--------------------------------------------------------------------------------
1 | OTHER_LDFLAGS = -framework CoreImage
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Artem Novichkov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | BINARY?=sasha
2 | BUILD_FOLDER?=.build
3 | OS?=sierra
4 | PREFIX?=/usr/local
5 | PROJECT?=Sasha
6 | RELEASE_BINARY_FOLDER?=$(BUILD_FOLDER)/release/$(PROJECT)
7 | VERSION?=2.4
8 |
9 | build:
10 | swift build --disable-sandbox -c release -Xswiftc -static-stdlib -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.12"
11 |
12 | test:
13 | swift test
14 |
15 | clean:
16 | swift package clean
17 | rm -rf $(BUILD_FOLDER) $(PROJECT).xcodeproj
18 |
19 | xcodeproj:
20 | swift package generate-xcodeproj --xcconfig-overrides Config.xcconfig
21 |
22 | install: build
23 | mkdir -p $(PREFIX)/bin
24 | cp -f $(RELEASE_BINARY_FOLDER) $(PREFIX)/bin/$(BINARY)
25 | cp -a .sasha ~
26 |
27 | bottle: clean build
28 | mkdir -p $(BINARY)/$(VERSION)/bin
29 | cp README.md $(BINARY)/$(VERSION)/README.md
30 | cp LICENSE $(BINARY)/$(VERSION)/LICENSE
31 | cp -f $(RELEASE_BINARY_FOLDER) $(BINARY)/$(VERSION)/bin/$(BINARY)
32 | tar cfvz $(BINARY)-$(VERSION).$(OS).bottle.tar.gz --exclude='*/.*' $(BINARY)
33 | shasum -a 256 $(BINARY)-$(VERSION).$(OS).bottle.tar.gz
34 | rm -rf $(BINARY)
35 |
36 | sha256:
37 | wget https://github.com/artemnovichkov/$(PROJECT)/archive/$(VERSION).tar.gz -O $(PROJECT)-$(VERSION).tar.gz
38 | shasum -a 256 $(PROJECT)-$(VERSION).tar.gz
39 | rm $(PROJECT)-$(VERSION).tar.gz
40 |
--------------------------------------------------------------------------------
/Package.pins:
--------------------------------------------------------------------------------
1 | {
2 | "autoPin": true,
3 | "pins": [
4 | {
5 | "package": "Files",
6 | "reason": null,
7 | "repositoryURL": "https://github.com/johnsundell/files.git",
8 | "version": "1.11.0"
9 | },
10 | {
11 | "package": "Swiftline",
12 | "reason": null,
13 | "repositoryURL": "https://github.com/nsomar/Swiftline.git",
14 | "version": "0.5.0"
15 | }
16 | ],
17 | "version": 1
18 | }
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "Files",
6 | "repositoryURL": "https://github.com/johnsundell/files.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "6171f9e9d8619678da22355560f3dbed6368058d",
10 | "version": "2.0.1"
11 | }
12 | },
13 | {
14 | "package": "SwiftPM",
15 | "repositoryURL": "https://github.com/apple/swift-package-manager.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "90abb3c4c9415afee34291e66e655c58a1902784",
19 | "version": "0.1.0"
20 | }
21 | }
22 | ]
23 | },
24 | "version": 1
25 | }
26 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:4.0
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "Sasha",
7 | dependencies: [
8 | .package(url: "https://github.com/johnsundell/files.git", from: "2.0.0"),
9 | .package(url: "https://github.com/apple/swift-package-manager.git", from: "0.1.0")
10 | ],
11 | targets: [
12 | .target(
13 | name: "Sasha",
14 | dependencies: ["SashaCore"]),
15 | .target(
16 | name: "SashaCore",
17 | dependencies: ["Files", "Utility"])
18 | ]
19 | )
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Sasha is an easy-to-use CLI app for routine designer tasks.
17 |
18 |
19 | Features • Using • Installing • Author • License
20 |
21 |
22 | ## Features
23 | - Icon slicing for different platforms:
24 | - iOS
25 | - watchOS including 40 and 44 mm versions
26 | - watchOS Complications
27 | - macOS
28 | - Carplay
29 | - Android
30 | - Android Wear
31 | - Project folder tree generation
32 |
33 | ## Using
34 |
35 | ### Icons
36 |
37 | Sasha has two main commands - `icons` and `project`.
38 |
39 | ```bash
40 | $ sasha icons --platform iOS --name icon.png
41 | ```
42 | Sasha generates icons in needed resolutions as well. For Apple platforms Sasha generates `AppIcon.appiconset`, which iOS developer can drag and drop right into `Images.xcassets` without manual icon sorting 👨🏻💻👍.
43 |
44 | There is a [service](https://github.com/artemnovichkov/sasha/issues/5#issuecomment-358310264) for Sasha. Right click on an original icon, select `Services > Sasha, make me iOS icons`.
45 |
46 | ### Project generation
47 | ```bash
48 | $ sasha project --name ProjectName
49 | ```
50 | Sasha generates folder tree with name passed via `--name` option. By default Sasha uses this project structure:
51 |
52 | ```
53 | iOS
54 | -UI
55 | --old
56 | --png
57 | -UX
58 | --old
59 | --png
60 | Android
61 | -UI
62 | --old
63 | --png
64 | -UX
65 | --old
66 | --png
67 | references
68 | -main_screens
69 | -menu
70 | -cards
71 | -another_case
72 | stuff
73 | -logos
74 | -icons
75 | -patterns
76 | -stocks
77 | -source
78 | ```
79 | To change it, open `~/.sasha/project.sasha` file in your favourite text editor and make custom project structure.
80 |
81 | ## Installing
82 |
83 | ### Homebrew (recommended):
84 |
85 | ```bash
86 | $ brew install artemnovichkov/projects/sasha
87 | ```
88 |
89 | ### Make:
90 |
91 | ```bash
92 | $ git clone https://github.com/artemnovichkov/sasha.git
93 | $ cd sasha
94 | $ make
95 | ```
96 |
97 | ### Swift Package Manager:
98 |
99 | ```bash
100 | $ git clone https://github.com/artemnovichkov/sasha.git
101 | $ cd sasha
102 | $ make build
103 | $ cp -f .build/release/sasha /usr/local/bin/sasha
104 | $ cp -r .sasha ~
105 | ```
106 |
107 | ### Marathon:
108 |
109 | - Install [Marathon](https://github.com/johnsundell/marathon#installing).
110 | - Add Files using `$ marathon add https://github.com/artemnovichkov/sasha.git`.
111 | - Run your script using `$ marathon run `.
112 |
113 | ## Author
114 |
115 | Artem Novichkov, novichkoff93@gmail.com
116 |
117 | [](https://www.codementor.io/artemnovichkov?utm_source=github&utm_medium=button&utm_term=artemnovichkov&utm_campaign=github)
118 |
119 |
120 | ## License
121 |
122 | Sasha is available under the MIT license. See the LICENSE file for more info.
123 |
124 |
--------------------------------------------------------------------------------
/Sources/Sasha/main.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Foundation
6 | import SashaCore
7 |
8 | var registry = CommandRegistry(usage: " ",
9 | overview: "👨💼 Reduce daily designer routine with sasha")
10 | registry.register(ProjectCommand.self, IconsCommand.self)
11 | registry.run()
12 |
--------------------------------------------------------------------------------
/Sources/SashaCore/Commands/Command.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Utility
6 |
7 | public protocol Command {
8 | var command: String { get }
9 | var overview: String { get }
10 |
11 | init(parser: ArgumentParser)
12 | func run(with arguments: ArgumentParser.Result) throws
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/SashaCore/Commands/CommandRegistry.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Foundation
6 | import Utility
7 | import Basic
8 |
9 | public struct CommandRegistry {
10 |
11 | private let parser: ArgumentParser
12 | private var commands: [Command] = []
13 |
14 | public init(usage: String, overview: String) {
15 | parser = ArgumentParser(usage: usage, overview: overview)
16 | }
17 |
18 | public mutating func register(_ command: Command.Type) {
19 | commands.append(command.init(parser: parser))
20 | }
21 |
22 | public mutating func register(_ commands: Command.Type...) {
23 | commands.forEach { register($0) }
24 | }
25 |
26 | public func run() {
27 | do {
28 | let arguments = try parse()
29 | try process(arguments)
30 | }
31 | catch {
32 | print(error)
33 | }
34 | }
35 |
36 | private func parse() throws -> ArgumentParser.Result {
37 | let arguments = Array(CommandLine.arguments.dropFirst())
38 | return try parser.parse(arguments)
39 | }
40 |
41 | private func process(_ arguments: ArgumentParser.Result) throws {
42 | guard let subparser = arguments.subparser(parser),
43 | let command = commands.first(where: { $0.command == subparser }) else {
44 | parser.printUsage(on: stdoutStream)
45 | return
46 | }
47 | try command.run(with: arguments)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/SashaCore/Commands/IconsCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Foundation
6 | import Utility
7 |
8 | public struct IconsCommand: Command {
9 |
10 | enum Error: Swift.Error {
11 | case missingOptions
12 | }
13 |
14 | public let command = "icons"
15 | public let overview = "Generates an icon set for selected platform."
16 | private let name: OptionArgument
17 | private let output: OptionArgument
18 | private let platform: OptionArgument
19 | private let idioms: OptionArgument<[Icon.Idiom]>
20 |
21 | public init(parser: ArgumentParser) {
22 | let subparser = parser.add(subparser: command, overview: overview)
23 | name = subparser.add(option: "--name",
24 | shortName: "-n",
25 | kind: String.self,
26 | usage: "Filename of original image. You can use full path of name of image file in current folder.")
27 | output = subparser.add(option: "--output",
28 | shortName: "-o",
29 | kind: String.self,
30 | usage: "Output path for generated icons.")
31 | platform = subparser.add(option: "--platform",
32 | shortName: "-p",
33 | kind: Platform.self,
34 | usage: "Platform for icon.")
35 | idioms = subparser.add(option: "--idioms",
36 | shortName: "-i",
37 | kind: [Icon.Idiom].self,
38 | usage: "A set of additional idioms.")
39 | }
40 |
41 | public func run(with arguments: ArgumentParser.Result) throws {
42 | guard let platform = arguments.get(platform),
43 | let fileName = arguments.get(name) else {
44 | throw Error.missingOptions
45 | }
46 | let idioms = arguments.get(self.idioms)
47 | let output = arguments.get(self.output)
48 | try IconsTask().generateIcons(for: platform, idioms: idioms, path: fileName, output: output)
49 | }
50 | }
51 |
52 | extension IconsCommand.Error: CustomStringConvertible {
53 |
54 | var description: String {
55 | switch self {
56 | case .missingOptions:
57 | return "Missing --platform or --name options."
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/SashaCore/Commands/ProjectCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Foundation
6 | import Utility
7 |
8 | public struct ProjectCommand: Command {
9 |
10 | enum Error: Swift.Error {
11 | case missingOptions
12 | }
13 |
14 | public let command = "project"
15 | public let overview = "Generates a project folders tree according to project.sasha file."
16 | private let projectName: OptionArgument
17 |
18 | public init(parser: ArgumentParser) {
19 | let subparser = parser.add(subparser: command, overview: overview)
20 | projectName = subparser.add(option: "--name",
21 | shortName: "-n",
22 | kind: String.self,
23 | usage: "Name of the project.")
24 | }
25 |
26 | public func run(with arguments: ArgumentParser.Result) throws {
27 | guard let projectName = arguments.get(projectName) else {
28 | throw Error.missingOptions
29 | }
30 | try ProjectTask().createProject(withName: projectName)
31 | }
32 | }
33 |
34 | extension ProjectCommand.Error: CustomStringConvertible {
35 |
36 | var description: String {
37 | switch self {
38 | case .missingOptions:
39 | return "Missing --name option."
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/SashaCore/Models/AndroidIcon.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Foundation
6 |
7 | final class AndroidIcon {
8 |
9 | let size: Float
10 | let name: String
11 |
12 | init(size: Float, name: String) {
13 | self.size = size
14 | self.name = name
15 | }
16 | }
17 |
18 | extension AndroidIcon: IconRepresentable {
19 |
20 | var iconSize: Float {
21 | return size
22 | }
23 |
24 | var iconName: String {
25 | return name
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/SashaCore/Models/AppIconSet.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Foundation
6 |
7 | final class AppIconSet: Codable {
8 |
9 | enum CodingKeys: String, CodingKey {
10 | case icons = "images"
11 | case info
12 | }
13 |
14 | let icons: [Icon]
15 | let info: Info
16 |
17 | init(icons: [Icon], info: Info = .default) {
18 | self.icons = icons
19 | self.info = info
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/SashaCore/Models/Asset.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Foundation
6 |
7 | final class Asset: Codable {
8 |
9 | let idiom: Icon.Idiom
10 | let filename: String
11 | let role: Icon.Role?
12 |
13 | init(idiom: Icon.Idiom,
14 | filename: String,
15 | role: Icon.Role? = nil) {
16 | self.idiom = idiom
17 | self.filename = filename
18 | self.role = role
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/SashaCore/Models/ComplicationSet.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Foundation
6 |
7 | final class ComplicationSet: Codable {
8 |
9 | let assets: [Asset]
10 | let info: Info
11 |
12 | init(assets: [Asset], info: Info = .default) {
13 | self.assets = assets
14 | self.info = info
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/SashaCore/Models/Icon.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Foundation
6 | import Utility
7 |
8 | final class Icon: Codable {
9 |
10 | enum CodingKeys: String, CodingKey {
11 | case idiom
12 | case size
13 | case filename
14 | case screenWidth = "screen-width"
15 | case scale
16 | case role
17 | case subtype
18 | }
19 |
20 | /// Idioms of icons.
21 | enum Idiom: String, Codable, ArgumentKind {
22 |
23 | case iphone
24 | case ipad
25 | case iosMarketing = "ios-marketing"
26 | case carplay = "car"
27 | case mac
28 | case watch
29 | case watchMarketing = "watch-marketing"
30 | case complicationCircular = "circular"
31 | case complicationExtraLarge = "extra large"
32 | case complicationGraphicBezel = "graphic bezel"
33 | case complicationGraphicCircular = "graphic circular"
34 | case complicationGraphicCorner = "graphic corner"
35 | case complicationGraphicLargeRectangular = "graphic large rectangular"
36 | case complicationModular = "modular"
37 | case complicationUtilitarian = "utilitarian"
38 |
39 | static let completion: ShellCompletion = .none
40 |
41 | static let complications: [Idiom] = [.complicationCircular,
42 | .complicationExtraLarge,
43 | .complicationGraphicBezel,
44 | .complicationGraphicCircular,
45 | .complicationGraphicCorner,
46 | .complicationGraphicLargeRectangular,
47 | .complicationModular,
48 | .complicationUtilitarian]
49 |
50 | init(argument: String) throws {
51 | guard let idiom = Idiom(rawValue: argument) else {
52 | throw Error.invalidIdiom
53 | }
54 | self = idiom
55 | }
56 | }
57 |
58 | enum Error: Swift.Error {
59 | case invalidIdiom
60 | }
61 |
62 | /// Roles of watchOS icons.
63 | enum Role: String, Codable {
64 | case notificationCenter
65 | case companionSettings
66 | case appLauncher
67 | case quickLook
68 | case circular
69 | case extraLarge = "extra-large"
70 | case graphicBezel = "graphic-bezel"
71 | case graphicCircular = "graphic-circular"
72 | case graphicCorner = "graphic-corner"
73 | case graphicLargeRectangular = "graphic-large-rectangular"
74 | case modular
75 | case utilitarian
76 | }
77 |
78 | /// Subtypes of watchOS icons with `notificationCenter` role.
79 | enum Subtype: String, Codable {
80 | case mm38 = "38mm", mm40 = "40mm", mm42 = "42mm", mm44 = "44mm"
81 | }
82 |
83 | let idiom: Idiom
84 | let size: Float
85 | let filename: String
86 | let screenWidth: String?
87 | let scale: Float
88 | let role: Role?
89 | let subtype: Subtype?
90 |
91 | init(idiom: Idiom,
92 | size: Float,
93 | filename: String,
94 | screenWidth: String? = nil,
95 | scale: Float,
96 | role: Role? = nil,
97 | subtype: Subtype? = nil) {
98 | self.idiom = idiom
99 | self.size = size
100 | let sizeString = String(format: "%g", size)
101 | let scaleString = String(format: "%g", scale)
102 | self.filename = filename + "-\(sizeString)x\(sizeString)@\(scaleString)x.png"
103 | self.screenWidth = screenWidth
104 | self.scale = scale
105 | self.role = role
106 | self.subtype = subtype
107 | }
108 |
109 | func encode(to encoder: Encoder) throws {
110 | var container = encoder.container(keyedBy: CodingKeys.self)
111 | let isComplication = Idiom.complications.contains(idiom)
112 | if isComplication {
113 | try container.encode(Idiom.watch, forKey: .idiom)
114 | }
115 | else {
116 | try container.encode(idiom, forKey: .idiom)
117 | }
118 | let scaleString = String(format: "%g", scale)
119 | if !isComplication {
120 | let sizeString = String(format: "%g", size)
121 | try container.encode("\(sizeString)x\(sizeString)", forKey: .size)
122 | }
123 | try container.encode(filename, forKey: .filename)
124 | if isComplication {
125 | try container.encodeIfPresent(screenWidth, forKey: .screenWidth)
126 | }
127 | try container.encode("\(scaleString)x", forKey: .scale)
128 | try container.encodeIfPresent(role, forKey: .role)
129 | try container.encodeIfPresent(subtype, forKey: .subtype)
130 | }
131 | }
132 |
133 | extension Icon: IconRepresentable {
134 |
135 | var iconSize: Float {
136 | return size * scale
137 | }
138 |
139 | var iconName: String {
140 | return filename
141 | }
142 | }
143 |
144 | extension Icon.Error: CustomStringConvertible {
145 |
146 | var description: String {
147 | switch self {
148 | case .invalidIdiom:
149 | return "Invalid idiom."
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/Sources/SashaCore/Models/IconFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Foundation
6 |
7 | final class IconFactory {
8 |
9 | /// Makes a set of icons. The set contains an icons only for selected idioms.
10 | ///
11 | /// - Parameters:
12 | /// - name: The name of generated icons.
13 | /// - idioms: The idioms of icons.
14 | /// - Returns: The set of icons.
15 | func makeSet(withName name: String, idioms: [Icon.Idiom]) -> AppIconSet {
16 | let icons = idioms.flatMap { idiom in
17 | makeIcons(forName: name, idiom: idiom)
18 | }
19 | return AppIconSet(icons: icons)
20 | }
21 |
22 | func makeComplicationSet() -> ComplicationSet {
23 | let assets = [Asset(idiom: .watch, filename: "Circular.imageset", role: .circular),
24 | Asset(idiom: .watch, filename: "Extra Large.imageset", role: .extraLarge),
25 | Asset(idiom: .watch, filename: "Graphic Bezel.imageset", role: .graphicBezel),
26 | Asset(idiom: .watch, filename: "Graphic Circular.imageset", role: .graphicCircular),
27 | Asset(idiom: .watch, filename: "Graphic Corner.imageset", role: .graphicCorner),
28 | Asset(idiom: .watch, filename: "Graphic Large Rectangular.imageset", role: .graphicLargeRectangular),
29 | Asset(idiom: .watch, filename: "Modular.imageset", role: .modular),
30 | Asset(idiom: .watch, filename: "Utilitarian.imageset", role: .utilitarian)]
31 | return ComplicationSet(assets: assets)
32 | }
33 |
34 | /// Makes an array of icons for Android. The names of icons contain full paths including needed folders.
35 | ///
36 | /// - Returns: The array of icons.
37 | func makeAndroidIcons() -> [AndroidIcon] {
38 | return [AndroidIcon(size: 72, name: "mipmap-hdpi/ic_launcher.png"),
39 | AndroidIcon(size: 36, name: "mipmap-ldpi/ic_launcher.png"),
40 | AndroidIcon(size: 48, name: "mipmap-mdpi/ic_launcher.png"),
41 | AndroidIcon(size: 96, name: "mipmap-xhdpi/ic_launcher.png"),
42 | AndroidIcon(size: 144, name: "mipmap-xxhdpi/ic_launcher.png"),
43 | AndroidIcon(size: 192, name: "mipmap-xxxhdpi/ic_launcher.png"),
44 | AndroidIcon(size: 512, name: "playstore-icon.png")]
45 | }
46 |
47 | /// Makes an array of icons for Android Wear. The names of icons contain full paths including needed folders.
48 | ///
49 | /// - Returns: The array of icons.
50 | func makeAndroidWearIcons() -> [AndroidIcon] {
51 | return [AndroidIcon(size: 72, name: "mipmap-hdpi/ic_launcher.png"),
52 | AndroidIcon(size: 48, name: "mipmap-mdpi/ic_launcher.png"),
53 | AndroidIcon(size: 96, name: "mipmap-xhdpi/ic_launcher.png"),
54 | AndroidIcon(size: 144, name: "mipmap-xxhdpi/ic_launcher.png")]
55 | }
56 |
57 | /// Makes a set of icons. The set contains an icons only for selected idiom.
58 | ///
59 | /// - Parameters:
60 | /// - name: The name of generated icons.
61 | /// - idiom: The idiom of icons.
62 | /// - Returns: The array of generated icons.
63 | private func makeIcons(forName name: String, idiom: Icon.Idiom) -> [Icon] {
64 | switch idiom {
65 | case .iphone:
66 | return [Icon(idiom: idiom, size: 20, filename: name, scale: 2),
67 | Icon(idiom: idiom, size: 20, filename: name, scale: 3),
68 | Icon(idiom: idiom, size: 29, filename: name, scale: 2),
69 | Icon(idiom: idiom, size: 29, filename: name, scale: 3),
70 | Icon(idiom: idiom, size: 40, filename: name, scale: 2),
71 | Icon(idiom: idiom, size: 40, filename: name, scale: 3),
72 | Icon(idiom: idiom, size: 60, filename: name, scale: 2),
73 | Icon(idiom: idiom, size: 60, filename: name, scale: 3)]
74 | case .ipad:
75 | return [Icon(idiom: idiom, size: 20, filename: name, scale: 1),
76 | Icon(idiom: idiom, size: 20, filename: name, scale: 2),
77 | Icon(idiom: idiom, size: 29, filename: name, scale: 1),
78 | Icon(idiom: idiom, size: 29, filename: name, scale: 2),
79 | Icon(idiom: idiom, size: 40, filename: name, scale: 1),
80 | Icon(idiom: idiom, size: 40, filename: name, scale: 2),
81 | Icon(idiom: idiom, size: 76, filename: name, scale: 1),
82 | Icon(idiom: idiom, size: 76, filename: name, scale: 2),
83 | Icon(idiom: idiom, size: 83.5, filename: name, scale: 2)]
84 | case .iosMarketing:
85 | return [Icon(idiom: idiom, size: 1024, filename: name, scale: 1)]
86 | case .carplay:
87 | return [Icon(idiom: idiom, size: 60, filename: name, scale: 2),
88 | Icon(idiom: idiom, size: 60, filename: name, scale: 3)]
89 | case .mac:
90 | return [Icon(idiom: idiom, size: 16, filename: name, scale: 1),
91 | Icon(idiom: idiom,size: 16, filename: name, scale: 2),
92 | Icon(idiom: idiom,size: 32, filename: name, scale: 1),
93 | Icon(idiom: idiom,size: 32, filename: name, scale: 2),
94 | Icon(idiom: idiom,size: 128, filename: name, scale: 1),
95 | Icon(idiom: idiom,size: 128, filename: name, scale: 2),
96 | Icon(idiom: idiom,size: 256, filename: name, scale: 1),
97 | Icon(idiom: idiom,size: 256, filename: name, scale: 2),
98 | Icon(idiom: idiom,size: 512, filename: name, scale: 1),
99 | Icon(idiom: idiom,size: 512, filename: name, scale: 2)]
100 | case .watch:
101 | return [Icon(idiom: idiom, size: 24, filename: name, scale: 2, role: .notificationCenter, subtype: .mm38),
102 | Icon(idiom: idiom, size: 27.5, filename: name, scale: 2, role: .notificationCenter, subtype: .mm42),
103 | Icon(idiom: idiom, size: 29, filename: name + "-Companion", scale: 2, role: .companionSettings),
104 | Icon(idiom: idiom, size: 29, filename: name + "-Companion", scale: 3, role: .companionSettings),
105 | Icon(idiom: idiom, size: 40, filename: name, scale: 2, role: .appLauncher, subtype: .mm38),
106 | Icon(idiom: idiom, size: 44, filename: name, scale: 2, role: .appLauncher, subtype: .mm40),
107 | Icon(idiom: idiom, size: 50, filename: name, scale: 2, role: .appLauncher, subtype: .mm44),
108 | Icon(idiom: idiom, size: 86, filename: name, scale: 2, role: .quickLook, subtype: .mm38),
109 | Icon(idiom: idiom, size: 98, filename: name, scale: 2, role: .quickLook, subtype: .mm42),
110 | Icon(idiom: idiom, size: 108, filename: name, scale: 2, role: .quickLook, subtype: .mm44)]
111 | case .watchMarketing:
112 | return [Icon(idiom: idiom, size: 1024, filename: name + "-AppStore", scale: 1)]
113 | case .complicationCircular:
114 | return [Icon(idiom: idiom, size: 16, filename: name + "-\(Icon.Subtype.mm38.rawValue)", screenWidth: "<=145", scale: 2),
115 | Icon(idiom: idiom, size: 18, filename: name + "-\(Icon.Subtype.mm42.rawValue)", screenWidth: ">161", scale: 2),
116 | Icon(idiom: idiom, size: 18, filename: name + "-\(Icon.Subtype.mm40.rawValue)", screenWidth: ">145", scale: 2),
117 | Icon(idiom: idiom, size: 18, filename: name + "-\(Icon.Subtype.mm44.rawValue)", screenWidth: ">183", scale: 2)]
118 | case .complicationExtraLarge:
119 | return [Icon(idiom: idiom, size: 91, filename: name + "-\(Icon.Subtype.mm38.rawValue)", screenWidth: "<=145", scale: 2),
120 | Icon(idiom: idiom, size: 101.5, filename: name + "-\(Icon.Subtype.mm42.rawValue)", screenWidth: ">161", scale: 2),
121 | Icon(idiom: idiom, size: 101.5, filename: name + "-\(Icon.Subtype.mm40.rawValue)", screenWidth: ">145", scale: 2),
122 | Icon(idiom: idiom, size: 112, filename: name + "-\(Icon.Subtype.mm44.rawValue)", screenWidth: ">183", scale: 2)]
123 | case .complicationGraphicBezel:
124 | return [Icon(idiom: idiom, size: 42, filename: name + "-\(Icon.Subtype.mm38.rawValue)", screenWidth: "<=145", scale: 2),
125 | Icon(idiom: idiom, size: 42, filename: name + "-\(Icon.Subtype.mm42.rawValue)", screenWidth: ">161", scale: 2),
126 | Icon(idiom: idiom, size: 42, filename: name + "-\(Icon.Subtype.mm40.rawValue)", screenWidth: ">145", scale: 2),
127 | Icon(idiom: idiom, size: 47, filename: name + "-\(Icon.Subtype.mm44.rawValue)", screenWidth: ">183", scale: 2)]
128 | case .complicationGraphicCircular:
129 | return [Icon(idiom: idiom, size: 42, filename: name + "-\(Icon.Subtype.mm38.rawValue)", screenWidth: "<=145", scale: 2),
130 | Icon(idiom: idiom, size: 42, filename: name + "-\(Icon.Subtype.mm42.rawValue)", screenWidth: ">161", scale: 2),
131 | Icon(idiom: idiom, size: 42, filename: name + "-\(Icon.Subtype.mm40.rawValue)", screenWidth: ">145", scale: 2),
132 | Icon(idiom: idiom, size: 47, filename: name + "-\(Icon.Subtype.mm44.rawValue)", screenWidth: ">183", scale: 2)]
133 | case .complicationGraphicCorner:
134 | return [Icon(idiom: idiom, size: 20, filename: name + "-\(Icon.Subtype.mm38.rawValue)", screenWidth: "<=145", scale: 2),
135 | Icon(idiom: idiom, size: 20, filename: name + "-\(Icon.Subtype.mm42.rawValue)", screenWidth: ">161", scale: 2),
136 | Icon(idiom: idiom, size: 20, filename: name + "-\(Icon.Subtype.mm40.rawValue)", screenWidth: ">145", scale: 2),
137 | Icon(idiom: idiom, size: 22, filename: name + "-\(Icon.Subtype.mm44.rawValue)", screenWidth: ">183", scale: 2)]
138 | case .complicationGraphicLargeRectangular:
139 | return []
140 | case .complicationModular:
141 | return [Icon(idiom: idiom, size: 26, filename: name + "-\(Icon.Subtype.mm38.rawValue)", screenWidth: "<=145", scale: 2),
142 | Icon(idiom: idiom, size: 29, filename: name + "-\(Icon.Subtype.mm42.rawValue)", screenWidth: ">161", scale: 2),
143 | Icon(idiom: idiom, size: 29, filename: name + "-\(Icon.Subtype.mm40.rawValue)", screenWidth: ">145", scale: 2),
144 | Icon(idiom: idiom, size: 32, filename: name + "-\(Icon.Subtype.mm44.rawValue)", screenWidth: ">183", scale: 2)]
145 | case .complicationUtilitarian:
146 | return [Icon(idiom: idiom, size: 20, filename: name + "-\(Icon.Subtype.mm38.rawValue)", screenWidth: "<=145", scale: 2),
147 | Icon(idiom: idiom, size: 22, filename: name + "-\(Icon.Subtype.mm42.rawValue)", screenWidth: ">161", scale: 2),
148 | Icon(idiom: idiom, size: 22, filename: name + "-\(Icon.Subtype.mm40.rawValue)", screenWidth: ">145", scale: 2),
149 | Icon(idiom: idiom, size: 25, filename: name + "-\(Icon.Subtype.mm44.rawValue)", screenWidth: ">183", scale: 2)]
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/Sources/SashaCore/Models/IconRepresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Foundation
6 |
7 | /// A protocol for iOS and Android icons representation.
8 | protocol IconRepresentable {
9 |
10 | var iconSize: Float { get }
11 | var iconName: String { get }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/SashaCore/Models/Info.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Foundation
6 |
7 | struct Info: Codable {
8 |
9 | let version: Int
10 | let author: String
11 |
12 | static let `default` = Info(version: 1, author: "sasha")
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/SashaCore/Models/Platform.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Foundation
6 | import Utility
7 |
8 | enum Platform: String, ArgumentKind, CaseIterable {
9 |
10 | enum Error: Swift.Error {
11 | case invalid
12 | }
13 |
14 | static let completion: ShellCompletion = .values(Platform.allCases.map { ($0.rawValue, $0.rawValue) })
15 |
16 | init(argument: String) throws {
17 | guard let platform = Platform(rawValue: argument.lowercased()) else {
18 | throw Error.invalid
19 | }
20 | self = platform
21 | }
22 |
23 | case iOS = "ios"
24 | case macOS = "macos"
25 | case watchOS = "watchos"
26 | case watchOSComplication = "complication"
27 | case android
28 | case androidWear = "androidwear"
29 | }
30 |
31 | extension Platform.Error: CustomStringConvertible {
32 |
33 | var description: String {
34 | switch self {
35 | case .invalid:
36 | return "Unsupported platform."
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/SashaCore/Services/FolderService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Foundation
6 |
7 | final class FolderService {
8 |
9 | enum Keys {
10 | static let slash = "/"
11 | }
12 |
13 | private class Folder {
14 | let name: String
15 | let level: Int
16 | var subfolders = [Folder]()
17 |
18 | init(name: String, level: Int) {
19 | self.name = name
20 | self.level = level
21 | }
22 |
23 | func add(_ folder: Folder) {
24 | if folder.level == level + 1 {
25 | subfolders.append(folder)
26 | return
27 | }
28 | subfolders.last?.add(folder)
29 | }
30 | }
31 |
32 | /// Generates the paths for project folders from configuration string.
33 | ///
34 | /// - Parameter string: The string from `sasha.project` file.
35 | /// - Returns: The array of full paths.
36 | func paths(fromString string: String) -> [String] {
37 | let components = string.components(separatedBy: "\n")
38 | let allFolders = components.map { component -> Folder in
39 | let name = component.replacingOccurrences(of: "-", with: "")
40 | let level = component.filter { $0 == "-" }
41 | return Folder(name: name, level: level.count)
42 | }
43 | var paths = [String]()
44 | var currentLevel = 0
45 | var currentPath: String?
46 | allFolders.forEach { folder in
47 | if let path = currentPath {
48 | if folder.level > currentLevel {
49 | currentPath = path + Keys.slash + folder.name
50 | }
51 | else {
52 | var components = path.components(separatedBy: Keys.slash)
53 | var subComponents = components[0.. CIImage {
143 | if let rep = NSImageRep(contentsOf: imageURL), !rep.isOpaque {
144 | print("\u{001B}[0;33mImage has transparency... filling regions...\u{001B}[0;0m")
145 | }
146 | guard let image = CIImage(contentsOf: imageURL) else {
147 | throw Error.imageCreationFailed
148 | }
149 | guard let width = image.properties[Keys.width] as? Float,
150 | let height = image.properties[Keys.height] as? Float else {
151 | throw Error.sizeReadingFailed
152 | }
153 | guard width == Keys.defaultSize, height == Keys.defaultSize else {
154 | throw Error.wrongSizeDetected(actualWidth: width,
155 | actualHeight: height,
156 | expectedWidth: Keys.defaultSize,
157 | expectedHeight: Keys.defaultSize)
158 | }
159 | return image
160 | }
161 |
162 | /// Generates icons for Apple platforms.
163 | ///
164 | /// - Parameter iconName: The name of generated icons.
165 | /// - Parameter imageURL: The url for original image.
166 | /// - Parameter idioms: Idioms for icons.
167 | /// - Parameter output: Output path for generated icons. Default value is nil.
168 | /// - Throws: `IconService.Error` errors.
169 | private func generateIcons(withIconName iconName: String,
170 | for imageURL: URL,
171 | idioms: [Icon.Idiom],
172 | output: String? = nil) throws {
173 | let image = try self.image(for: imageURL)
174 | let iconSet = iconFactory.makeSet(withName: iconName,
175 | idioms: idioms)
176 | var folderName = Keys.iconSetName
177 | if let output = output {
178 | folderName = output + folderName
179 | }
180 | try generateIcons(from: image, icons: iconSet.icons, folderName: folderName)
181 | var path = Keys.iconSetName + "/" + Keys.contentsName
182 | if let output = output {
183 | path = output + path
184 | }
185 | try writeContents(of: iconSet, path: path)
186 | }
187 |
188 | /// Writes contents file that contains names of icons.
189 | ///
190 | /// - Parameter set: The set with icons.
191 | /// - Parameter path: Path for generated icons.
192 | /// - Throws: `File.Error.writeFailed` if the file couldn’t be written to.
193 | private func writeContents(of set: AppIconSet, path: String) throws {
194 | encoder.outputFormatting = .prettyPrinted
195 | let iconSetData = try encoder.encode(set)
196 | let contentsFile = try fileSystem.createFile(at: path)
197 | try contentsFile.write(data: iconSetData)
198 | }
199 |
200 | /// Writes contents file that contains names of icons.
201 | ///
202 | /// - Parameter set: The set with assets.
203 | /// - Parameter path: Path for generated icons.
204 | /// - Throws: `File.Error.writeFailed` if the file couldn’t be written to.
205 | private func writeContents(of set: ComplicationSet, path: String) throws {
206 | encoder.outputFormatting = .prettyPrinted
207 | let iconSetData = try encoder.encode(set)
208 | let contentsFile = try fileSystem.createFile(at: path)
209 | try contentsFile.write(data: iconSetData)
210 | }
211 |
212 | /// Generates icons from original image. The image is resized and written to image files.
213 | ///
214 | /// - Parameters:
215 | /// - image: The original image.
216 | /// - icons: The array of objects, that represent icon information.
217 | /// - folderName: The name of folder for generated icons.
218 | /// - Throws: `IconService.Error` errors.
219 | private func generateIcons(from image: CIImage, icons: [IconRepresentable], folderName: String) throws {
220 | let image = image.settingAlphaOne(in: image.extent)
221 | let filter = CIFilter(name: Keys.lanczosFilterName)!
222 | filter.setValue(image, forKey: kCIInputImageKey)
223 | filter.setValue(1, forKey: kCIInputAspectRatioKey)
224 |
225 | let context = CIContext(options: [kCIContextUseSoftwareRenderer: false])
226 | let colorSpace = CGColorSpaceCreateDeviceRGB()
227 |
228 | try icons.forEach { icon in
229 | let scale = icon.iconSize / Keys.defaultSize
230 | filter.setValue(scale, forKey: kCIInputScaleKey)
231 | guard let outputImage = filter.value(forKey: kCIOutputImageKey) as? CIImage else {
232 | throw Error.imageRenderingFailed
233 | }
234 | guard let outputData = context.representation(of: outputImage, colorSpace: colorSpace) else {
235 | try fileSystem.currentFolder.subfolder(named: folderName).delete()
236 | throw Error.imageRenderingFailed
237 | }
238 | let path = [folderName, icon.iconName].joined(separator: "/")
239 | let file = try fileSystem.createFile(at: path)
240 | try file.write(data: outputData)
241 | }
242 | }
243 | }
244 |
245 | extension IconService.Error: CustomStringConvertible {
246 |
247 | var description: String {
248 | switch self {
249 | case .imageCreationFailed: return "Can't create an image."
250 | case .sizeReadingFailed: return "Can't get image size."
251 | case let .wrongSizeDetected(actualWidth, actualHeight, expectedWidth, expectedHeight):
252 | let actualString = String(format: "%g", actualWidth) + "x" + String(format: "%g", actualHeight)
253 | let expectedString = String(format: "%g", expectedWidth) + "x" + String(format: "%g", expectedHeight)
254 | return "Image has wrong size (actual \(actualString), expected \(expectedString))."
255 | case .imageRenderingFailed: return "Can't render the image."
256 | }
257 | }
258 | }
259 |
260 | extension CIContext {
261 |
262 | func representation(of image: CIImage,
263 | format: CIFormat = kCIFormatRGBA8,
264 | colorSpace: CGColorSpace,
265 | options: [AnyHashable: Any] = [:]) -> Data? {
266 | if #available(OSX 10.13, *) {
267 | return pngRepresentation(of: image, format: format, colorSpace: colorSpace)
268 | }
269 | else if #available(OSX 10.12, *) {
270 | return jpegRepresentation(of: image, colorSpace: colorSpace)
271 | }
272 | return nil
273 | }
274 | }
275 |
--------------------------------------------------------------------------------
/Sources/SashaCore/Tasks/IconsTask.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Foundation
6 | import Files
7 |
8 | final class IconsTask {
9 |
10 | private let iconService: IconService
11 |
12 | init(iconService: IconService = IconService()) {
13 | self.iconService = iconService
14 | }
15 |
16 | func generateIcons(for platform: Platform,
17 | idioms: [Icon.Idiom]? = nil,
18 | path: String,
19 | output: String? = nil) throws {
20 | let file: File
21 | if path.contains("/") {
22 | file = try File(path: path)
23 | }
24 | else {
25 | file = try Folder.current.file(named: path)
26 | }
27 | let url = URL(fileURLWithPath: file.path)
28 | switch platform {
29 | case .iOS:
30 | try iconService.generateiOSIcons(for: url, idioms: idioms, output: output)
31 | print("🎉 AppIcon.appiconset was successfully created")
32 | case .macOS:
33 | try iconService.generateMacOSIcons(for: url, output: output)
34 | print("🎉 AppIcon.appiconset was successfully created")
35 | case .watchOS:
36 | try iconService.generateWatchOSIcons(for: url, output: output)
37 | print("🎉 AppIcon.appiconset was successfully created")
38 | case .watchOSComplication:
39 | try iconService.generateWatchOSComplicationIcons(for: url, output: output)
40 | print("🎉 Complication.complicationset was successfully created")
41 | case .android:
42 | try iconService.generateAndroidIcons(for: url)
43 | print("🎉 Icons were successfully created")
44 | case .androidWear:
45 | try iconService.generateAndroidWearIcons(for: url)
46 | print("🎉 Icons were successfully created")
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/SashaCore/Tasks/ProjectTask.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2017 Artem Novichkov. All rights reserved.
3 | //
4 |
5 | import Foundation
6 | import Files
7 |
8 | final class ProjectTask {
9 |
10 | enum Error: Swift.Error {
11 | case sketchTemplatesCreationFailed
12 | }
13 |
14 | private enum Keys {
15 | static let fileTemplates = "~/.sasha/file_templates/"
16 | }
17 |
18 | private let folderService: FolderService
19 | private let fileSystem: FileSystem
20 |
21 | init(folderService: FolderService = FolderService(),
22 | fileSystem: FileSystem = FileSystem()) {
23 | self.folderService = folderService
24 | self.fileSystem = fileSystem
25 | }
26 |
27 | func createProject(withName name: String) throws {
28 | let projectFile = try File(path: "~/.sasha/project.sasha")
29 | let projectString = try projectFile.readAsString()
30 |
31 | let paths = folderService.paths(fromString: projectString)
32 | try paths.forEach { path in
33 | let finalPath = name + FolderService.Keys.slash + path
34 | try fileSystem.createFolder(at: finalPath)
35 | }
36 |
37 | let projectFolder = try Folder.current.subfolder(named: name)
38 | do {
39 | try addSketchFiles(to: projectFolder, projectName: name.lowercased())
40 | }
41 | catch {
42 | throw Error.sketchTemplatesCreationFailed
43 | }
44 |
45 | print("🎉 Project \(name) was successfully created.")
46 | }
47 |
48 | private func addSketchFiles(to folder: Folder, projectName: String) throws {
49 | //TODO: Hardcoded paths. Think about it.
50 | try Platform.allCases.forEach { platform in
51 | let platformName = platform.rawValue
52 | let sketchFile = try File(path: Keys.fileTemplates + "\(platformName.lowercased())_template.sketch")
53 | let sketchFileData = try sketchFile.read()
54 | let fileName = "\(platformName)/UI/" + projectName + " -\(platformName.lowercased()).sketch"
55 | try folder.createFile(named: fileName, contents: sketchFileData)
56 | }
57 | }
58 | }
59 |
60 | extension ProjectTask.Error: CustomStringConvertible {
61 |
62 | var description: String {
63 | switch self {
64 | case .sketchTemplatesCreationFailed:
65 | return "Can't create Sketch project files from templates. Please check ~/.sasha/file_templates folder."
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------