瀏覽代碼

add local initials avatar implementation

Signed-off-by: Henrik Storch <thisisthefoxe@gmail.com>
Henrik Storch 3 年之前
父節點
當前提交
7b69760e57

+ 2 - 0
Nextcloud.xcodeproj/project.pbxproj

@@ -17,6 +17,7 @@
 		370D26AF248A3D7A00121797 /* NCCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370D26AE248A3D7A00121797 /* NCCellProtocol.swift */; };
 		371B5A2E23D0B04500FAFAE9 /* NCMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B5A2D23D0B04500FAFAE9 /* NCMenu.swift */; };
 		3781B9B023DB2B7E006B4B1D /* AppDelegate+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3781B9AF23DB2B7E006B4B1D /* AppDelegate+Menu.swift */; };
+		D575039F27146F93008DC9DC /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A0D1342591FBC5008F8A13 /* String+Extensions.swift */; };
 		F700222C1EC479840080073F /* Custom.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F700222B1EC479840080073F /* Custom.xcassets */; };
 		F700222D1EC479840080073F /* Custom.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F700222B1EC479840080073F /* Custom.xcassets */; };
 		F700510122DF63AC003A3356 /* NCShare.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F700510022DF63AC003A3356 /* NCShare.storyboard */; };
@@ -2040,6 +2041,7 @@
 				2C1D5D7523E2DE3300334ABB /* NCDatabase.swift in Sources */,
 				2C1D5D7623E2DE3300334ABB /* NCManageDatabase.swift in Sources */,
 				2C33C48223E2C475005F963B /* NotificationService.swift in Sources */,
+				D575039F27146F93008DC9DC /* String+Extensions.swift in Sources */,
 				F73D5E4A246DE09200DF6467 /* NCElementsJSON.swift in Sources */,
 				F79B646326CA661600838ACA /* UIControl+Extensions.swift in Sources */,
 				F798F0EC2588060A000DAFFD /* UIColor+Extensions.swift in Sources */,

+ 1 - 1
Share/NCShareExtension.swift

@@ -240,7 +240,7 @@ class NCShareExtension: UIViewController, NCListCellDelegate, NCEmptyDataSetDele
 
         // PROFILE BUTTON
 
-        let image = NCUtility.shared.loadUserImage(for: activeAccount.user, urlBase: activeAccount.urlBase)
+        let image = NCUtility.shared.loadUserImage(for: activeAccount.user, displayName: activeAccount.displayName, urlBase: activeAccount.urlBase)
 
         let profileButton = UIButton(type: .custom)
         profileButton.setImage(image, for: .normal)

+ 34 - 0
iOSClient/Brand/NCBrand.swift

@@ -427,4 +427,38 @@ class NCBrandColor: NSObject {
             NCBrandColor.shared.brandElement = NCBrandColor.shared.brand
         }
     }
+    
+    func stepCalc(steps: Int, color1: CGColor, color2: CGColor) -> [CGFloat] {
+        var step = [CGFloat](repeating: 0, count: 3)
+        step[0] = (color2.components![0] - color1.components![0]) / CGFloat(steps)
+        step[1] = (color2.components![1] - color1.components![1]) / CGFloat(steps)
+        step[2] = (color2.components![2] - color1.components![2]) / CGFloat(steps)
+        return step
+    }
+    
+    func mixPalette(steps: Int, color1: CGColor, color2: CGColor) -> [CGColor] {
+        var palette = [color1]
+        let step = stepCalc(steps: steps, color1: color1, color2: color2)
+        
+        let c1Components = color1.components!
+        for i in 1 ..< steps {
+            let r = c1Components[0] + step[0] * CGFloat(i)
+            let g = c1Components[1] + step[1] * CGFloat(i)
+            let b = c1Components[2] + step[2] * CGFloat(i)
+            
+            palette.append(UIColor(red: r, green: g, blue: b, alpha: 1).cgColor)
+        }
+        return palette
+    }
+    
+    func genColors(steps: Int = 6) -> [CGColor] {
+        let red = UIColor(red: 182/255, green: 70/255, blue: 157/255, alpha: 1).cgColor
+        let yellow = UIColor(red: 221/255, green: 203/255, blue: 85/255, alpha: 1).cgColor
+        let blue = UIColor(red: 0/255, green: 130/255, blue: 201/255, alpha: 1).cgColor
+
+        let palette1 = mixPalette(steps: steps, color1: red, color2: yellow)
+        let palette2 = mixPalette(steps: steps, color1: yellow, color2: blue)
+        let palette3 = mixPalette(steps: steps, color1: blue, color2: red)
+        return palette1 + palette2 + palette3
+    }
 }

+ 28 - 0
iOSClient/Extensions/String+Extensions.swift

@@ -23,8 +23,17 @@
 
 import Foundation
 import UIKit
+import CommonCrypto
 
 extension String {
+    public var uppercaseInitials: String? {
+        let initials = self.components(separatedBy: .whitespaces)
+            .reduce("", {
+                guard $0.count < 2, let nextLetter = $1.first else { return $0 }
+                return $0 + nextLetter.uppercased()
+            })
+        return initials.isEmpty ? nil : initials
+    }
     
     func formatSecondsToString(_ seconds: TimeInterval) -> String {
         if seconds.isNaN {
@@ -36,4 +45,23 @@ extension String {
         return String(format: "%02d:%02d:%02d", hour, min, sec)
     }
     
+    func md5() -> String {
+        //https://stackoverflow.com/a/32166735/9506784
+
+        let length = Int(CC_MD5_DIGEST_LENGTH)
+        let messageData = self.data(using:.utf8)!
+        var digestData = Data(count: length)
+        
+        _ = digestData.withUnsafeMutableBytes { digestBytes -> UInt8 in
+            messageData.withUnsafeBytes { messageBytes -> UInt8 in
+                if let messageBytesBaseAddress = messageBytes.baseAddress, let digestBytesBlindMemory = digestBytes.bindMemory(to: UInt8.self).baseAddress {
+                    let messageLength = CC_LONG(messageData.count)
+                    CC_MD5(messageBytesBaseAddress, messageLength, digestBytesBlindMemory)
+                }
+                return 0
+            }
+        }
+        
+        return digestData.map { String(format: "%02hhx", $0) }.joined()
+    }
 }

+ 1 - 1
iOSClient/Main/Account Request/NCAccountRequest.swift

@@ -236,7 +236,7 @@ extension NCAccountRequest: UITableViewDataSource {
         
             let account = accounts[indexPath.row]
 
-            avatarImage?.image = NCUtility.shared.loadUserImage(for: account.user, urlBase: account.urlBase)
+            avatarImage?.image = NCUtility.shared.loadUserImage(for: account.user, displayName: account.displayName, urlBase: account.urlBase)
 
             if account.alias != "" {
                 userLabel?.text = account.alias.uppercased()

+ 2 - 2
iOSClient/Main/Collection Common/NCCollectionViewCommon.swift

@@ -591,15 +591,15 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS
             // PROFILE BUTTON
             
             if layoutKey == NCGlobal.shared.layoutViewFiles {
+                let activeAccount = NCManageDatabase.shared.getActiveAccount()
 
-                let image = NCUtility.shared.loadUserImage(for: appDelegate.user, urlBase: appDelegate.urlBase)
+                let image = NCUtility.shared.loadUserImage(for: appDelegate.user, displayName: activeAccount?.displayName, urlBase: appDelegate.urlBase)
 
                 let button = UIButton(type: .custom)
                 button.setImage(image, for: .normal)
                 
                 if serverUrl == NCUtilityFileSystem.shared.getHomeServer(account: appDelegate.account) {
                  
-                    let activeAccount = NCManageDatabase.shared.getActiveAccount()
                     var titleButton = "  "
 
                     if getNavigationTitle() == activeAccount?.alias {

+ 1 - 1
iOSClient/Menu/NCLoginWeb+Menu.swift

@@ -38,7 +38,7 @@ extension NCLoginWeb {
             
             let title = account.user + " " + (URL(string: account.urlBase)?.host ?? "")
 
-            avatar = NCUtility.shared.loadUserImage(for: account.user, urlBase: account.urlBase)
+            avatar = NCUtility.shared.loadUserImage(for: account.user, displayName: account.displayName, urlBase: account.urlBase)
 
             actions.append(
                 NCMenuAction(

+ 1 - 1
iOSClient/More/NCMore.swift

@@ -323,7 +323,7 @@ class NCMore: UIViewController, UITableViewDelegate, UITableViewDataSource {
             cell.status.text = ""
             cell.displayName.text = ""
 
-            cell.avatar.image = NCUtility.shared.loadUserImage(for: appDelegate.user, urlBase: appDelegate.urlBase)
+            cell.avatar.image = NCUtility.shared.loadUserImage(for: appDelegate.user, displayName: tabAccount?.displayName, urlBase: appDelegate.urlBase)
 
             if let account = tabAccount {
                 if account.alias == "" {

+ 31 - 1
iOSClient/NCGlobal.swift

@@ -29,6 +29,37 @@ class NCGlobal: NSObject {
         return instance
     }()
 
+    func usernameToColor(_ username: String) -> CGColor {
+        // Normalize hash
+        let loweUserName = username.lowercased()
+        var hash: String
+        
+        let regex = try! NSRegularExpression(pattern: "^([0-9a-f]{4}-?){8}$")
+        let matches = regex.matches(in: username,
+                                    range: NSRange(username.startIndex..., in: username))
+        
+        if (!matches.isEmpty) {
+            // Already a md5 hash?
+            // done, use as is.
+            hash = loweUserName
+        } else {
+            hash = loweUserName.md5()
+        }
+
+        hash = hash.replacingOccurrences(of: "[^0-9a-f]", with: "", options: .regularExpression)
+        
+        let steps = 6
+        let finalPalette = NCBrandColor.shared.genColors(steps: steps)
+        
+        return finalPalette[NCGlobal.hashToInt(hash: hash, maximum: steps * 3)]
+    }
+    
+    // Convert a string to an integer evenly
+    // hash is hex string
+    static func hashToInt(hash: String, maximum: Int) -> Int {
+        let result = hash.compactMap(\.hexDigitValue)
+        return result.reduce(0, { $0 + $1 }) % maximum
+    }
     // Struct for Progress
     //
     struct progressType {
@@ -353,4 +384,3 @@ if let popoverController = alertController.popoverPresentationController {
 
 @discardableResult
 */
-

+ 53 - 19
iOSClient/Utility/NCUtility.swift

@@ -39,24 +39,6 @@ class NCUtility: NSObject {
     private var viewActivityIndicator: UIView?
     private var viewBackgroundActivityIndicator: UIView?
 
-    func loadUserImage(for user: String, urlBase: String) -> UIImage {
-        var image: UIImage
-        
-        let fileName = String(CCUtility.getUserUrlBase(user, urlBase: urlBase)) + "-" + user + "-original.png"
-        let fileNameLocalPath = String(CCUtility.getDirectoryUserData()) + "/" + fileName
-        if let imageUser = UIImage(contentsOfFile: fileNameLocalPath) {
-            image = NCUtility.shared.createAvatar(image: imageUser, size: 30)
-        } else {
-            if #available(iOS 13.0, *) {
-                let config = UIImage.SymbolConfiguration(pointSize: 30)
-                image = NCUtility.shared.loadImage(named: "person.crop.circle", symbolConfiguration: config)
-            } else {
-                image = NCUtility.shared.loadImage(named: "person.crop.circle", size: 30)
-            }
-        }
-        return image
-    }
-
     func setLayoutForView(key: String, serverUrl: String, layoutForView: NCGlobal.layoutForViewType) {
         
         let string =  layoutForView.layout + "|" + layoutForView.sort + "|" + "\(layoutForView.ascending)" + "|" + layoutForView.groupBy + "|" + "\(layoutForView.directoryOnTop)" + "|" + layoutForView.titleButtonHeader + "|" + "\(layoutForView.itemForLine)" + "|" + layoutForView.imageBackgroud + "|" + layoutForView.imageBackgroudContentMode
@@ -402,7 +384,9 @@ class NCUtility: NSObject {
                 
         return(onlineStatus, statusMessage, descriptionMessage)
     }
-    
+
+    // MARK: -
+
     func imageFromVideo(url: URL, at time: TimeInterval) -> UIImage? {
         
         let asset = AVURLAsset(url: url)
@@ -514,6 +498,30 @@ class NCUtility: NSObject {
         return  UIImage(named: "file")!.image(color: color, size: size)
     }
     
+    func loadUserImage(for user: String, displayName: String?, urlBase: String) -> UIImage {
+        var image: UIImage?
+        
+        let fileName = String(CCUtility.getUserUrlBase(user, urlBase: urlBase)) + "-" + user + "-original.png"
+        let fileNameLocalPath = String(CCUtility.getDirectoryUserData()) + "/" + fileName
+        if let imageUser = UIImage(contentsOfFile: fileNameLocalPath) {
+            image = NCUtility.shared.createAvatar(image: imageUser, size: 30)
+        } else if let displayName = displayName, !displayName.isEmpty {
+            image = createAvatar(displayName: displayName, size: 30)
+        } else {
+            // fallback to default icon
+        }
+        return image ?? getDefaultUserIcon()
+    }
+    
+    func getDefaultUserIcon() -> UIImage {
+        if #available(iOS 13.0, *) {
+            let config = UIImage.SymbolConfiguration(pointSize: 30)
+            return NCUtility.shared.loadImage(named: "person.crop.circle", symbolConfiguration: config)
+        } else {
+            return NCUtility.shared.loadImage(named: "person.crop.circle", size: 30)
+        }
+    }
+    
     @objc func createAvatar(image: UIImage, size: CGFloat) -> UIImage {
         
         var avatarImage = image
@@ -528,6 +536,32 @@ class NCUtility: NSObject {
         return avatarImage
     }
     
+    func createAvatar(displayName: String, size: CGFloat) -> UIImage? {
+        guard let initials = displayName.uppercaseInitials else {
+            return nil
+        }
+        let userColor = NCGlobal.shared.usernameToColor(displayName)
+        let rect = CGRect(x: 0, y: 0, width: size, height: size)
+        var avatarImage: UIImage?
+        
+        UIGraphicsBeginImageContextWithOptions(rect.size, false, 3.0)
+        let context = UIGraphicsGetCurrentContext()
+        UIBezierPath.init(roundedRect: rect, cornerRadius: rect.size.height).addClip()
+        context?.setFillColor(userColor)
+        context?.fill(rect)
+        let textStyle = NSMutableParagraphStyle()
+        textStyle.alignment = NSTextAlignment.center
+        let lineHeight = UIFont.systemFont(ofSize: UIFont.systemFontSize).pointSize
+        NSString(string: initials)
+            .draw(
+                in: CGRect(x: 0, y: (size - lineHeight) / 2, width: size, height: lineHeight),
+                withAttributes: [NSAttributedString.Key.paragraphStyle: textStyle])
+        avatarImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+
+        return avatarImage
+    }
+
     // MARK: -
 
     @objc func startActivityIndicator(backgroundView: UIView?, blurEffect: Bool, bottom: CGFloat = 0, style: UIActivityIndicatorView.Style = .whiteLarge) {