From ec2573be8313ef8b4bc92f46440abe195590a508 Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:58:19 +0530 Subject: [PATCH 01/40] NMC 1984 - Privacy policy customisation added --- .../dataPrivacy.imageset/Contents.json | 23 ++ .../dataPrivacy.imageset/default copy@2x.png | Bin 0 -> 1262 bytes .../dataPrivacy.imageset/default copy@3x.png | Bin 0 -> 2037 bytes .../dataPrivacy.imageset/default copy@4x.png | Bin 0 -> 2624 bytes .../NCCollectionViewCommon.swift | 21 ++ .../AnalysisDataCollectionSwitch.swift | 34 +++ .../Settings/AnalysisDataCollectionSwitch.xib | 73 ++++++ ...InitialPrivacySettingsViewController.swift | 170 +++++++++++++ iOSClient/Settings/NCSettings.storyboard | 236 ++++++++++++++++++ .../PrivacyPolicyViewController.swift | 62 +++++ .../PrivacySettingsViewController.swift | 133 ++++++++++ .../RequiredDataCollectionSwitch.swift | 34 +++ .../Settings/RequiredDataCollectionSwitch.xib | 73 ++++++ .../SaveSettingsCustomButtonCell.swift | 46 ++++ .../Settings/SaveSettingsCustomButtonCell.xib | 46 ++++ 15 files changed, 951 insertions(+) create mode 100644 iOSClient/Images.xcassets/dataPrivacy.imageset/Contents.json create mode 100644 iOSClient/Images.xcassets/dataPrivacy.imageset/default copy@2x.png create mode 100644 iOSClient/Images.xcassets/dataPrivacy.imageset/default copy@3x.png create mode 100644 iOSClient/Images.xcassets/dataPrivacy.imageset/default copy@4x.png create mode 100644 iOSClient/Settings/AnalysisDataCollectionSwitch.swift create mode 100644 iOSClient/Settings/AnalysisDataCollectionSwitch.xib create mode 100644 iOSClient/Settings/InitialPrivacySettingsViewController.swift create mode 100644 iOSClient/Settings/NCSettings.storyboard create mode 100644 iOSClient/Settings/PrivacyPolicyViewController.swift create mode 100644 iOSClient/Settings/PrivacySettingsViewController.swift create mode 100644 iOSClient/Settings/RequiredDataCollectionSwitch.swift create mode 100644 iOSClient/Settings/RequiredDataCollectionSwitch.xib create mode 100644 iOSClient/Settings/SaveSettingsCustomButtonCell.swift create mode 100644 iOSClient/Settings/SaveSettingsCustomButtonCell.xib diff --git a/iOSClient/Images.xcassets/dataPrivacy.imageset/Contents.json b/iOSClient/Images.xcassets/dataPrivacy.imageset/Contents.json new file mode 100644 index 0000000000..4fb0d4c09c --- /dev/null +++ b/iOSClient/Images.xcassets/dataPrivacy.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "default copy@2x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "default copy@3x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "default copy@4x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOSClient/Images.xcassets/dataPrivacy.imageset/default copy@2x.png b/iOSClient/Images.xcassets/dataPrivacy.imageset/default copy@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..40c3e7d47f2c2fdbdc87713870b113c36d2bbc07 GIT binary patch literal 1262 zcmVPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NGSV=@dRA>e5naPV4R}{wkIf1Ao z5ETSbL~s#eB7z#VK@nFX?NU*9iDKMmoppq41lN*(AQ}`zT`5kjAWpQPIDkV2g9?$D zIHdFYy6Y4tRrgxAhVFLZf$!Z@x6b#Sb8ZdqRlUBx4>qwY(Cae&@bK_d^p=4&4oY#( z;YzR9d#%YXtW^UPJewj`11oq1{`88%Ub6oZu%^z_&>t0?=Hkd&&H*Ulfk>}IXf=^ zHFF*e($JrbB?~q{@mGla3{)k0LCheqVlTuJ5i?@h!e}w9DJ{~>(Y?`6EzAwHb$$KM zVhzL&-$U41UBgvGCEnO*NQ?9ZP(z0KAwhBl>n+71>g@m<+rh$8;22FPSC&(`a|@*l zb|i)cCwFaxj>oE6CUu-|+~qzrFz!oUNH``(fb(`UI1YZzAb)cq_tC!Be}>~V4(FfJR5@$+P2$AQZ2>bM$`k6pySyWly_A8*&^eiBL^ zYrr2aJ%mS%75WJL0PX>^pK$b!XvE|L&6(Z9*$1kgg?;WvrdK&4{53E;93v2*yUw5m z7RHe#m@5|V!%nj??H&4uUGVN$TxlNCB1e3RA3Rdb?{#q@S55ep55;^{Du4+BCjsR) zTM~+k&cV84Oh&y1DH-!8Tuf8Q0Rf1ro?!(9M$cNa=3iA@uRodq|6Ac2md zYw$YRq>*VcjF@hp`uC@MLwL_Tv$r^VQtN0HN1|QewPnsmz5lHI!KE0WSZ{iS9ov;Jc1*$zz#o1U^bhzRl&%fU zW&0H4O^>hrMg4KvCh=V0f3sU*y4a5Zy`>i9Yp!VG>c8fRHHz2-^sSj{cM04wu~iqc zQ_vK!16%_!yTuM&Kl$TkLFWC~q`-clZ*P4e>D9`QU?T|rsrY_W9jJnWCxa=0U7bz+ Y2SkK6zRJ=a>Hq)$07*qoM6N<$g2I134*&oF literal 0 HcmV?d00001 diff --git a/iOSClient/Images.xcassets/dataPrivacy.imageset/default copy@3x.png b/iOSClient/Images.xcassets/dataPrivacy.imageset/default copy@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..74d9dbb7dd8e51db1844e2102dff7045c235a1fb GIT binary patch literal 2037 zcmVPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS?Ur9tkRCodHn{TXFa~#LJLd_y{ zMZ^}8G@*Y&>~7Q(tF1iI^x!h{$e*z+p5dAC#0+a>nR$@4qHI=AG}TQcim5Bg5W|(4 zq+7SH*Zb$3+d1F+J?DG=pL_Xzw)g%1{d_*}@B4TDpL2%{Xfe=HnCjx)#P&uKXRdo`_-@;VxLbTq3Vu!*MCJ=&*Ao7jC7i1>&b3@t6YaAr> zN5B_tD}o&^B3)*I5M&$#d@)pB3x?((^)>6=B=mfaQEme|7&pKIy1cPQLC#hxI%xZtM*YmjV75QM;MOM?^-sy!oh_8- z1YTK=vqQ?dJV3??6P5V8bb< z18rdvOYoMk zfq7rZaPMmRmmt(Pl(krG#~>DeJ1f==>jGekBQeycv2EU5^;K@Q2*iSQ&$J4ROt50j zS>^d8m|pCZS_EPV&w+DNEJbtWQ@K?x5L?1Uta4t4mCo~gGq`4VsaznawgKBQ$F&Oi z$hT6fL!hxaR+z^(Q`$NN%ChzNi_7Ed$4FJyA<)mM7=|ZTe&JY$KwHCWPSw&2#7eDl zfw(Hx^XkqFtK5gL9(26PFeFDpzNWp-p~d&d*qu*Mya z_A)-3xu0jrVezqXx2>_y)g_Sa1Aq(qymMAq_)bFH03HT9yJ>4V1a!7~3`}N@?)Zx` z8~0d&T+f3_6-cW_fWIkp0IuR7Y!k?~Mp0{+YYW=%PYjf{B!S@pUA1xlF^;iIARPlt z8*O$BqPC6ucX9k(0!iKDMgV;Wl*%}J3@)V_d(4F&)g8PtbId z$Y(@*B^c4Gk4X}hxvk(^i@(_w5{W^VIKKavRP?mbY0MkrI|a&RE=SicCVzd&VOpEn z5z$^u|K$;5!R|9?tbn+L(Jr?FsLy4Sn4n99B-Un`l>g=z!j}aU_p3`S$wFOBYysc< z4hc(bNpZWCEsSnwzHTD>DQv4RFUX9M$; zE_(WeFZRB%GxN9sZZIhF3*R2+RS;O=h(J0t?Ef zp&VW=ijW}GwopH2F4s)$WbG*5>vlK%O+s$9|otGN|$vpp?%N@5l57ToCjV4 zQ3r5_5}Quz%Rp;f&OtfGqOJ$}#}wPO3*An8pBXa-<>J$jW|3Ee*MaTciSCf;x@I+) znI^}7VlImw10DrifoXbn{1g~l=VAxUvt)$O^BR5FsVnX^ytCR~r!dXj2^IJUOCpgG TJQa3{00000NkvXXu0mjfv1qa| literal 0 HcmV?d00001 diff --git a/iOSClient/Images.xcassets/dataPrivacy.imageset/default copy@4x.png b/iOSClient/Images.xcassets/dataPrivacy.imageset/default copy@4x.png new file mode 100644 index 0000000000000000000000000000000000000000..682122c3f21e9c196b2df4c47179f97e87b9788b GIT binary patch literal 2624 zcmV-G3cvMPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91V4wp41ONa40RR91U;qFB0I4%yP5=N2s!2paRCodHok^?|RTPGKf+#3Y zFc_SIKtPQX4iU%bMx#d5#5e#NbYWt2;l@Oa!Hq*q+!&2R^3~5tNQk{Px7a_Pn~Wa^m(Y+^X4A0R zN*X}14}j(wpt0a%!cwo?+d3ljH-yih(3EYVqXuaJ1wR5}>7hhLSMi62CW}99sh@)n z6D(jUIUk&t;AECn>{;9K`w2*!qLcetzLyg322gAzcM3?iZWVrfDHis@8(Fm&sNgF0 zM|sC;`03oD^GVtioo5aPRmf_b>Ku|bWf}amkvNNU14!(CysGF^KvfC-h;J1--NHZf zO^C5gl58-OQKox{8go>WQMfPKU_J&5;eD@*Tb~AmzHHkK!N$YMhmc<5Lb|LCO*sZ1 zX+yNsVQas#ejCS=i&Jf#jN};7WgD;wH|gO-Teyy)f5~^dh17cCphIce6dkq>D#I&= z2K+<<>7hhRJtEjyCaJo{(vTs<(j$+wAzB{k6Tj{T=y37{NDICI0)0M&W4EO#gBQ90Iza|H`Xn8#|W3) zdS){XTn6+=JblTdmnX}>Ti|)F!J~M_1eb7Pyxp_hgoKN_&l6$x19ySnL7vD)wzcft zHZq}Wu=i=GDOm78Ee?s<3*Iut&e3U%_Lxe=#vypn+5$B&C+1*Kr1&XajjiJKWIxL^ zQ0ubaAk>G}0O|Ko{!M;}F)NKzVmZs?JCx4G%hi9h593H|6iH}mNZGkUngcX}6Z0Rg zMe(sy15EY<;bGjhBv!Wgs*QZeSCmrAl|sSHFE2RGG%TbqlmEiHm>ZxsPN5{EuTY%! z`iZ##hX>3wq_2RqGTvzhjZ(~+^96!~`;fjI!nvEoya%jzPb1edq_5myJ_a#2;5(m0 za%}r@*0*hS%ng_yuqy8d^cB=q%nf)mV5aW`^cB=qv<=`+@>4R^%;U168F`w$9Hm6t zfTUUv1*qD?0erOIMcIc=MKd7o3%1w}dcB~jRBRlA2dyno3v=R*24CCaXXw|k?x+@b z!N1z#R}pBFCD^dw!CD*=_h_)v65pi$0p-LNH^ILWW0)y8y3MY^e9i%y-*1wGOv;ct zPNF~H?{x5tgWnghnar4#4z^`YY`NQNRXiSDU2W<1*y3LON5bJG@F@5c`~fzArQmUJ zn!m^5BZxN^m}E_Hua=QS`P>uJ~5;tOvV6 zqkP5}{rQ?qkqubZIFQfsC7e?HKaOrB&mfx)KY zn|v-tsXwPsSQy^}l)Fhv`8JXwiY7^IG(|%<$vDVC$=D0QcHk<;CNNmV*E&~GE`={! z89T`mXe;{t9a8rr_H3384;-|j9=Y#&xh_^6qX6mG6 zslP+yEI#J|Iki0=)@@)&~-Eih2`9`i_Ch1Ypu9w>4@ax~*+gCb(fc}WpxPGd;+ zik5DtUv(_>nuCeG?JhF)>9W+G;B(k9`PALC*YhmwbvU*i|F3Z>W)UNT4hNfoNiOj5 zi38g%Z1P>TPQN*D7RX#+A*6Fzud^czy$(S<;L=mRcP#;?Tl6+{;S-?EVF~?No?~sf zen5@9ejMV}fQ0007P8anzR#{~yN0&7uT<^KCCSm&fH!@)<@-;21~ptq@< zleRessQ2Nb98G^4V>kMj19GyvVk>wBm}G%{)WBFn(hE$9qkFry3<-9Lg@n{zC*#^k z2MDCxGV;DYKnOP%kkD=TDhru8ZeBXiihSo-a@Nh{0kvjCz^tDSO{tYk4Z+BFilz8c zKTrdO<`4w!1-6@!XeUB7XW{;hTo(e9XtILt#_JaX&o(hibTjH_R82(knURs_Bh^H8 z@CvD{y+9ATMmV^M+C(`3$!cQ-ANlsE%%w@F;*I9R@DV~Sp{ z8$0HFAg&2#7Dky{pr_Rif#wn57bD$$O5MZ^sg^}~@8Sq-H(bg@?+^4MCq9ALm13A7 zPsYc(nGxKd%;2w8H#U!m>>C81$VuRy$SC*If%iQ*+@I?=j`afxXv+5Tj6`;`F1oEq zoPfC5L{nLO-gmUGMG!p}{AtSs{Vs54`xrJy7MFw*3#0U*9n(B2@^28qP*#mo{F-}dgeE~;VTkjU*>nI`LWeO=w0nj{s)0E(3*ntF42nCv6* zLP9ryZCP0->t13R{Ddc;K)&-t7Y_sTwJM2S2y8ELSk*7T^66Gk`p#KC5?}I}Cz`?@ z(|o1yyAKH-55CH)M6zCXR~BWVvA{Lf5^Tp8jnz@(NK{iCv!1iFx=C~sbREc@e=3{A zR%2Y{F${-xR1rgvCFyA(>xQkhfVv^lP1Aq=1etWnI@!32uJL^)lZ8k-E*})&81O3a zk)0U73v{0$=(BV_@;b)0AKArKq&nD84E;-}6+RU#Xsf>=aaA2TbzBSzqt}~yBI$Ej z!rVzVV)Y;x-f@Mgn=nNh3Dk&npsc9y4}-(%&ZFbwQM3br?xI%#Us=n5`#`twWw!*1 zs7p)zJnC8SH?WBwbI%33WNq71*0T9VJ)Xkq9zq3v1#AU+Xf_|*0seD&8g(+APL6{2 if>E8GN0)yaEAS7aJVK+!9r$4Y0000P$ literal 0 HcmV?d00001 diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 7db4928ad4..af4f6d4ff4 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -264,6 +264,14 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let dropInteraction = UIDropInteraction(delegate: self) self.navigationController?.navigationItem.leftBarButtonItems?.first?.customView?.addInteraction(dropInteraction) + + if(!UserDefaults.standard.bool(forKey: "isInitialPrivacySettingsShowed") || isApplicationUpdated()){ + redirectToPrivacyViewController() + + //set current app version + let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String + UserDefaults.standard.set(appVersion, forKey: "CurrentAppVersion") + } registerForTraitChanges([UITraitUserInterfaceStyle.self]) { [weak self] (view: NCCollectionViewCommon, _) in guard let self else { return } @@ -377,6 +385,19 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, removeImageCache(metadatas: self.dataSource.getMetadatas()) } + + func isApplicationUpdated() -> Bool{ + let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" + let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") + return currentVersion != appVersion + } + + func redirectToPrivacyViewController(){ + let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) + let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController + newViewController.modalPresentationStyle = .fullScreen + self.present(newViewController, animated: true, completion: nil) + } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) diff --git a/iOSClient/Settings/AnalysisDataCollectionSwitch.swift b/iOSClient/Settings/AnalysisDataCollectionSwitch.swift new file mode 100644 index 0000000000..e24216220f --- /dev/null +++ b/iOSClient/Settings/AnalysisDataCollectionSwitch.swift @@ -0,0 +1,34 @@ +// +// AnalysisDataCollectionSwitch.swift +// Nextcloud +// +// Created by A200073704 on 25/04/23. +// Copyright © 2023 Marino Faggiana. All rights reserved. +// + +import UIKit + +class AnalysisDataCollectionSwitch: XLFormBaseCell { + + @IBOutlet weak var cellLabel: UILabel! + @IBOutlet weak var analysisDataCollectionSwitchControl: UISwitch! + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + analysisDataCollectionSwitchControl.addTarget(self, action: #selector(switchChanged), for: UIControl.Event.valueChanged) + } + + override func configure() { + super.configure() + } + + override func update() { + super.update() + } + + @objc func switchChanged(mySwitch: UISwitch) { + self.rowDescriptor.value = mySwitch.isOn + } +} + diff --git a/iOSClient/Settings/AnalysisDataCollectionSwitch.xib b/iOSClient/Settings/AnalysisDataCollectionSwitch.xib new file mode 100644 index 0000000000..45530193f6 --- /dev/null +++ b/iOSClient/Settings/AnalysisDataCollectionSwitch.xib @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOSClient/Settings/InitialPrivacySettingsViewController.swift b/iOSClient/Settings/InitialPrivacySettingsViewController.swift new file mode 100644 index 0000000000..2cd30a4242 --- /dev/null +++ b/iOSClient/Settings/InitialPrivacySettingsViewController.swift @@ -0,0 +1,170 @@ +// +// InitialPrivacySettingsViewController.swift +// Nextcloud +// +// Created by A200073704 on 25/04/23. +// Copyright © 2023 Marino Faggiana. All rights reserved. +// + +import Foundation +import AppTrackingTransparency +import AdSupport +import UIKit + +class InitialPrivacySettingsViewController: UIViewController { + + @IBOutlet weak var dataPrivacyImage: UIImageView! + @IBOutlet weak var acceptButton: UIButton! + @IBOutlet weak var privacySettingsHelpText: UITextView! + @IBOutlet weak var privacySettingsTitle: UILabel! + @IBOutlet weak var widthPriavacyHelpView: NSLayoutConstraint! + var privacyHelpText = "" + + override func viewDidLoad() { + super.viewDidLoad() + + privacySettingsTitle.text = NSLocalizedString("_privacy_settings_title_", comment: "") + privacyHelpText = NSLocalizedString("_privacy_help_text_after_login_", comment: "") + privacySettingsHelpText.text = privacyHelpText + dataPrivacyImage.image = UIImage(named: "dataPrivacy")!.image(color: NCBrandColor.shared.brand, size: 60) + privacySettingsHelpText.delegate = self + privacySettingsHelpText.textColor = .label + privacySettingsHelpText.hyperLink(originalText: privacyHelpText, + linkTextsAndTypes: [NSLocalizedString("_key_privacy_help_", comment: ""): LinkType.privacyPolicy.rawValue, + NSLocalizedString("_key_reject_help_", comment: ""): LinkType.reject.rawValue, + NSLocalizedString("_key_settings_help_", comment: ""): LinkType.settings.rawValue]) + + acceptButton.backgroundColor = NCBrandColor.shared.brand + acceptButton.tintColor = UIColor.white + acceptButton.layer.cornerRadius = 5 + acceptButton.layer.borderWidth = 1 + acceptButton.layer.borderColor = NCBrandColor.shared.brand.cgColor + acceptButton.setTitle(NSLocalizedString("_accept_button_title_", comment: ""), for: .normal) + privacySettingsHelpText.centerText() + privacySettingsHelpText.font = UIFont(name: privacySettingsHelpText.font!.fontName, size: 16) + self.navigationItem.leftBarButtonItem?.tintColor = NCBrandColor.shared.brand + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.navigationBar.isHidden = true + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + self.navigationController?.navigationBar.isHidden = false + } + + override func viewDidLayoutSubviews(){ + if UIDevice.current.userInterfaceIdiom == .pad { + widthPriavacyHelpView.constant = UIScreen.main.bounds.width - 100 + } + } + + @IBAction func onAcceptButtonClicked(_ sender: Any) { + requestPermission() + } + + //NEWLY ADDED PERMISSIONS FOR iOS 14 + func requestPermission() { + UserDefaults.standard.set(true, forKey: "isInitialPrivacySettingsShowed") + UserDefaults.standard.set(true, forKey: "isAnalysisDataCollectionSwitchOn") + if #available(iOS 14, *) { + ATTrackingManager.requestTrackingAuthorization { status in + switch status { + case .authorized: + // Tracking authorization dialog was shown + // and we are authorized + print("Authorized") + // Now that we are authorized we can get the IDFA + print(ASIdentifierManager.shared().advertisingIdentifier) + case .denied: + UserDefaults.standard.set(true, forKey: "isInitialPrivacySettingsShowed") + UserDefaults.standard.set(false, forKey: "isAnalysisDataCollectionSwitchOn") + print("Denied") + case .notDetermined: + // Tracking authorization dialog has not been shown + print("Not Determined") + case .restricted: + print("Restricted") + @unknown default: + print("Unknown") + } + } + } else { + UserDefaults.standard.set(true, forKey: "isInitialPrivacySettingsShowed") + UserDefaults.standard.set(true, forKey: "isAnalysisDataCollectionSwitchOn") + } + self.dismiss(animated: true, completion: nil) + } +} +// MARK: - UITextViewDelegate +extension InitialPrivacySettingsViewController: UITextViewDelegate { + func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool { + if let linkType = LinkType(rawValue: URL.absoluteString) { + // TODO: handle linktype here with switch or similar. + switch linkType { + case LinkType.privacyPolicy: + //let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) + let privacyViewController = PrivacyPolicyViewController() + self.navigationController?.pushViewController(privacyViewController, animated: true) + case LinkType.reject: + UserDefaults.standard.set(false, forKey: "isAnalysisDataCollectionSwitchOn") + UserDefaults.standard.set(true, forKey: "isInitialPrivacySettingsShowed") + self.dismiss(animated: true, completion: nil) + case LinkType.settings: + let privacySettingsViewController = PrivacySettingsViewController() + UserDefaults.standard.set(true, forKey: "showSettingsButton") + self.navigationController?.pushViewController(privacySettingsViewController, animated: true) + } + print("handle link:: \(linkType)") + } + return false + } +} + +public extension UITextView { + + func hyperLink(originalText: String, linkTextsAndTypes: [String: String]) { + + let style = NSMutableParagraphStyle() + style.alignment = .left + + let attributedOriginalText = NSMutableAttributedString(string: originalText) + + let fullRange = NSRange(location: 0, length: attributedOriginalText.length) + attributedOriginalText.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.label, range: fullRange) + for linkTextAndType in linkTextsAndTypes { + let linkRange = attributedOriginalText.mutableString.range(of: linkTextAndType.key) + attributedOriginalText.addAttribute(NSAttributedString.Key.link, value: linkTextAndType.value, range: linkRange) + attributedOriginalText.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: fullRange) + attributedOriginalText.addAttribute(NSAttributedString.Key.foregroundColor, value: NCBrandColor.shared.brand, range: linkRange) + attributedOriginalText.addAttribute(NSAttributedString.Key.font, value: UIFont.systemFont(ofSize: 10), range: fullRange) + } + + self.linkTextAttributes = [NSAttributedString.Key.foregroundColor: NCBrandColor.shared.brand] + self.attributedText = attributedOriginalText + } + + func centerText() { + self.textAlignment = .justified + let fittingSize = CGSize(width: 300, height: CGFloat.greatestFiniteMagnitude) + let size = sizeThatFits(fittingSize) + let topOffset = (bounds.size.height - size.height * zoomScale) / 2 + let positiveTopOffset = max(1, topOffset) + contentOffset.y = -positiveTopOffset + } +} + +enum LinkType: String { + case reject + case privacyPolicy + case settings +} + + diff --git a/iOSClient/Settings/NCSettings.storyboard b/iOSClient/Settings/NCSettings.storyboard new file mode 100644 index 0000000000..19a320515b --- /dev/null +++ b/iOSClient/Settings/NCSettings.storyboard @@ -0,0 +1,236 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOSClient/Settings/PrivacyPolicyViewController.swift b/iOSClient/Settings/PrivacyPolicyViewController.swift new file mode 100644 index 0000000000..141c444fca --- /dev/null +++ b/iOSClient/Settings/PrivacyPolicyViewController.swift @@ -0,0 +1,62 @@ +// +// PrivacyPolicyViewController.swift +// Nextcloud +// +// Created by A200073704 on 25/04/23. +// Copyright © 2023 Marino Faggiana. All rights reserved. +// + +import Foundation +import UIKit +import WebKit + +class PrivacyPolicyViewController: UIViewController, WKNavigationDelegate, WKUIDelegate { + + var myWebView = WKWebView() + + override func viewDidLoad() { + super.viewDidLoad() + + self.title = NSLocalizedString("_privacy_policy_", comment: "") + + myWebView = WKWebView(frame: CGRect(x:0, y:0, width: UIScreen.main.bounds.width, height:UIScreen.main.bounds.height)) + myWebView.uiDelegate = self + myWebView.navigationDelegate = self + myWebView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.view.addSubview(myWebView) + + //1. Load web site into my web view + let myURL = URL(string: "https://static.magentacloud.de/privacy/datenschutzhinweise_app.htm") + let myURLRequest:URLRequest = URLRequest(url: myURL!) + NCActivityIndicator.shared.start() + myWebView.load(myURLRequest) + self.navigationController?.navigationBar.tintColor = NCBrandColor.shared.brand + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + myWebView = WKWebView(frame: CGRect(x:0, y:0, width: UIScreen.main.bounds.width, height:UIScreen.main.bounds.height)) + } + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + NCActivityIndicator.shared.stop() + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if navigationAction.navigationType == .linkActivated { + if let url = navigationAction.request.url, + UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + decisionHandler(.cancel) + } else { + decisionHandler(.allow) + } + } else { + decisionHandler(.allow) + } + } +} diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift new file mode 100644 index 0000000000..3e60dd6212 --- /dev/null +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -0,0 +1,133 @@ +// +// PrivacySettingsViewController.swift +// Nextcloud +// +// Created by A200073704 on 25/04/23. +// Copyright © 2023 Marino Faggiana. All rights reserved. +// + +import Foundation +import AppTrackingTransparency +import AdSupport + +class PrivacySettingsViewController: XLFormViewController{ + + @objc public var isShowSettingsButton: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + self.title = NSLocalizedString("_privacy_settings_title_", comment: "") + + NotificationCenter.default.addObserver(self, selector: #selector(changeTheming), name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterChangeTheming), object: nil) + + let nib = UINib(nibName: "CustomSectionHeader", bundle: nil) + self.tableView.register(nib, forHeaderFooterViewReuseIdentifier: "customSectionHeader") + isShowSettingsButton = UserDefaults.standard.bool(forKey: "showSettingsButton") + self.navigationController?.navigationBar.tintColor = NCBrandColor.shared.brand + changeTheming() + } + + @objc func changeTheming() { + tableView.backgroundColor = .systemGroupedBackground + tableView.separatorColor = .none + tableView.separatorColor = .clear + tableView.reloadData() + initializeForm() + } + + //MARK: XLForm + func initializeForm() { + let form : XLFormDescriptor = XLFormDescriptor() as XLFormDescriptor + form.rowNavigationOptions = XLFormRowNavigationOptions.stopDisableRow + + var section : XLFormSectionDescriptor + var row : XLFormRowDescriptor + + // Section: Destination Folder + + section = XLFormSectionDescriptor.formSection(withTitle: NSLocalizedString("", comment: "").uppercased()) + section.footerTitle = " " + form.addFormSection(section) + + section = XLFormSectionDescriptor.formSection(withTitle: NSLocalizedString("", comment: "").uppercased()) + section.footerTitle = NSLocalizedString("_privacy_settings_help_text_", comment: "") + form.addFormSection(section) + + //custom cell + section = XLFormSectionDescriptor.formSection(withTitle: "") + section.footerTitle = NSLocalizedString("_required_data_collection_help_text_", comment: "") + form.addFormSection(section) + + XLFormViewController.cellClassesForRowDescriptorTypes()["RequiredDataCollectionCustomCellType"] = RequiredDataCollectionSwitch.self + + row = XLFormRowDescriptor(tag: "ButtonDestinationFolder", rowType: "RequiredDataCollectionCustomCellType", title: "") + row.cellConfig["requiredDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand + row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue + row.cellConfig["cellLabel.font"] = UIFont.systemFont(ofSize: 15.0) + row.cellConfig["cellLabel.textColor"] = UIColor.label //photos + row.cellConfig["cellLabel.text"] = NSLocalizedString("_required_data_collection_", comment: "") + section.addFormRow(row) + + section = XLFormSectionDescriptor.formSection(withTitle: NSLocalizedString("", comment: "").uppercased()) + section.footerTitle = NSLocalizedString("_analysis_data_acqusition_help_text_", comment: "") + form.addFormSection(section) + + XLFormViewController.cellClassesForRowDescriptorTypes()["AnalysisDataCollectionCustomCellType"] = AnalysisDataCollectionSwitch.self + + row = XLFormRowDescriptor(tag: "AnalysisDataCollectionSwitch", rowType: "AnalysisDataCollectionCustomCellType", title: "") + row.cellConfig["analysisDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand + row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue + row.cellConfig["cellLabel.font"] = UIFont.systemFont(ofSize: 15.0) + row.cellConfig["cellLabel.textColor"] = UIColor.label //photos + row.cellConfig["cellLabel.text"] = NSLocalizedString("_analysis_data_acqusition_", comment: "") + if(UserDefaults.standard.bool(forKey: "isAnalysisDataCollectionSwitchOn")){ + row.cellConfigAtConfigure["analysisDataCollectionSwitchControl.on"] = 1 + }else { + row.cellConfigAtConfigure["analysisDataCollectionSwitchControl.on"] = 0 + } + + section.addFormRow(row) + + XLFormViewController.cellClassesForRowDescriptorTypes()["SaveSettingsButton"] = SaveSettingsCustomButtonCell.self + + section = XLFormSectionDescriptor.formSection(withTitle: "") + form.addFormSection(section) + + row = XLFormRowDescriptor(tag: "SaveSettingsButton", rowType: "SaveSettingsButton", title: "") + row.cellConfig["backgroundColor"] = UIColor.clear + + if(isShowSettingsButton){ + section.addFormRow(row) + } + + self.form = form + } + + override func formRowDescriptorValueHasChanged(_ formRow: XLFormRowDescriptor!, oldValue: Any!, newValue: Any!) { + super.formRowDescriptorValueHasChanged(formRow, oldValue: oldValue, newValue: newValue) + + if formRow.tag == "SaveSettingsButton" { + print("save settings clicked") + //TODO save button state and leave the page + self.navigationController?.popViewController(animated: true) + + } + if formRow.tag == "AnalysisDataCollectionSwitch"{ + if (formRow.value! as AnyObject).boolValue { + if #available(iOS 14, *) { + ATTrackingManager.requestTrackingAuthorization(completionHandler: { (status) in + if status == .denied { + guard let url = URL(string: UIApplication.openSettingsURLString) else { + return + } + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:]) + } + } + }) + } + } + UserDefaults.standard.set((formRow.value! as AnyObject).boolValue, forKey: "isAnalysisDataCollectionSwitchOn") + } + } +} diff --git a/iOSClient/Settings/RequiredDataCollectionSwitch.swift b/iOSClient/Settings/RequiredDataCollectionSwitch.swift new file mode 100644 index 0000000000..f80aa97e89 --- /dev/null +++ b/iOSClient/Settings/RequiredDataCollectionSwitch.swift @@ -0,0 +1,34 @@ +// +// RequiredDataCollectionSwitch.swift +// Nextcloud +// +// Created by A200073704 on 25/04/23. +// Copyright © 2023 Marino Faggiana. All rights reserved. +// + +import UIKit + + +class RequiredDataCollectionSwitch: XLFormBaseCell { + + @IBOutlet weak var cellLabel: UILabel! + @IBOutlet weak var requiredDataCollectionSwitchControl: UISwitch! + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + //requiredDataCollectionSwitchControl.addTarget(self, action: #selector(switchChanged), for: UIControl.Event.valueChanged) + + } + + override func configure() { + super.configure() + + requiredDataCollectionSwitchControl.isOn = true + requiredDataCollectionSwitchControl.isEnabled = false + } + + override func update() { + super.update() + } +} diff --git a/iOSClient/Settings/RequiredDataCollectionSwitch.xib b/iOSClient/Settings/RequiredDataCollectionSwitch.xib new file mode 100644 index 0000000000..66156c6e06 --- /dev/null +++ b/iOSClient/Settings/RequiredDataCollectionSwitch.xib @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOSClient/Settings/SaveSettingsCustomButtonCell.swift b/iOSClient/Settings/SaveSettingsCustomButtonCell.swift new file mode 100644 index 0000000000..6205727723 --- /dev/null +++ b/iOSClient/Settings/SaveSettingsCustomButtonCell.swift @@ -0,0 +1,46 @@ +// +// SaveSettingsCustomButtonCell.swift +// Nextcloud +// +// Created by A200073704 on 25/04/23. +// Copyright © 2023 Marino Faggiana. All rights reserved. +// + +import UIKit + + +class SaveSettingsCustomButtonCell: XLFormButtonCell { + + @IBOutlet weak var saveSettingsButton: UIButton! + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + self.selectionStyle = .none + self.separatorInset = UIEdgeInsets(top: 0, left: .greatestFiniteMagnitude, bottom: 0, right: .greatestFiniteMagnitude) + saveSettingsButton.setTitle(NSLocalizedString("_save_settings_", comment: ""), for: .normal) + saveSettingsButton.addTarget(self, action: #selector(saveButtonClicked), for: .touchUpInside) + + } + + override func configure() { + super.configure() + saveSettingsButton.backgroundColor = NCBrandColor.shared.brand + saveSettingsButton.tintColor = UIColor.white + saveSettingsButton.layer.cornerRadius = 5 + saveSettingsButton.layer.borderWidth = 1 + saveSettingsButton.layer.borderColor = NCBrandColor.shared.brand.cgColor + + } + + override func update() { + super.update() + + } + + @objc func saveButtonClicked(sender: UIButton) { + self.rowDescriptor.value = sender + + } + +} diff --git a/iOSClient/Settings/SaveSettingsCustomButtonCell.xib b/iOSClient/Settings/SaveSettingsCustomButtonCell.xib new file mode 100644 index 0000000000..0dd197da31 --- /dev/null +++ b/iOSClient/Settings/SaveSettingsCustomButtonCell.xib @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From d5a42a2f371077167ea719a06cf7c2fd51dd9559 Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:58:46 +0530 Subject: [PATCH 02/40] NMC 1984 - Unit test added for privacy policy view controller --- .../PrivacyPolicyTest.swift | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 Tests/NextcloudUnitTests/PrivacyPolicyTest.swift diff --git a/Tests/NextcloudUnitTests/PrivacyPolicyTest.swift b/Tests/NextcloudUnitTests/PrivacyPolicyTest.swift new file mode 100644 index 0000000000..f6265f48e7 --- /dev/null +++ b/Tests/NextcloudUnitTests/PrivacyPolicyTest.swift @@ -0,0 +1,154 @@ +// +// PrivacyPolicyTest.swift +// NextcloudTests +// +// Created by A200073704 on 27/04/23. +// Copyright © 2023 Marino Faggiana. All rights reserved. +// + +@testable import Nextcloud +import XCTest +import NextcloudKit +import XLForm + + + class PrivacyPolicyTest: XCTestCase { + + var viewController: InitialPrivacySettingsViewController? + var privacySettingsView = PrivacySettingsViewController() + + override func setUpWithError() throws { + + // To Create an instance of UIStoryboard + let storyboard = UIStoryboard(name: "NCSettings", bundle: nil) + + // To Instantiate UIViewController with Storyboard ID + viewController = storyboard.instantiateViewController(withIdentifier: "privacyPolicyViewController") as? InitialPrivacySettingsViewController + + // Outlets are connected + let _ = viewController?.view + + // Make the viewDidLoad() execute. + viewController?.loadViewIfNeeded() + + } + + override func tearDownWithError() throws { + viewController = nil + } + + func testPrivacyPolicyViewControllerIsOpen() { + + // Check that the InitialPrivacyPolicyViewController gets opened + let storyboard = UIStoryboard(name: "NCSettings", bundle: nil) + if let privacyPolicyViewController = storyboard.instantiateViewController(withIdentifier: "privacyPolicyViewController") as? InitialPrivacySettingsViewController { + let navigationController = UINavigationController(rootViewController: privacyPolicyViewController) + + privacyPolicyViewController.loadViewIfNeeded() + + XCTAssertTrue(navigationController.topViewController is InitialPrivacySettingsViewController, "Privacy policy view controller should be open") + } + } + + func testTextViewHasCorrectText() { + + //Check that the text displayed is correct + let expectedText = NSLocalizedString("_privacy_help_text_after_login_", comment: "") + viewController?.privacySettingsHelpText?.text = expectedText + + let actualText = viewController?.privacySettingsHelpText?.text + XCTAssertEqual(actualText, expectedText, "The text view does not have the expected text") + } + + func testHasAcceptButton() { + + // Check that view has the accept button + let acceptButton = viewController?.acceptButton + + XCTAssertNotNil(acceptButton, "View controller does not have an accept button") + + } + + func testSettingsLinkTypeNavigatesToPrivacySettingsViewController() { + + // Simulate tapping the "Settings" link type + let linkType = LinkType.settings + + UserDefaults.standard.set(true, forKey: "showSettingsButton") + viewController?.privacySettingsHelpText.hyperLink(originalText: viewController?.privacyHelpText ?? "", linkTextsAndTypes: [NSLocalizedString("_key_settings_help_", comment: ""): linkType.rawValue]) + + // Check that the correct view controller was pushed onto the navigation stack + XCTAssertNotNil(viewController?.navigationController?.visibleViewController is PrivacySettingsViewController) + } + + func testPrivacyPolicyLinkType_NavigatesToPrivacyPolicyViewController() { + + // Simulate tapping the "Privacy Policy" link type + let linkType = LinkType.privacyPolicy + + viewController?.privacySettingsHelpText.hyperLink(originalText: viewController?.privacyHelpText ?? "", linkTextsAndTypes: [NSLocalizedString("_key_privacy_help_", comment: ""): linkType.rawValue]) + + // Check that the correct view controller was pushed onto the navigation + XCTAssertNotNil(viewController?.navigationController?.visibleViewController is PrivacyPolicyViewController) + } + + func testCorrectImagePresentOnInitialPrivacySettingsViewController() { + + // Check that the image view has the correct image + let expectedImage = UIImage(named: "dataPrivacy") + XCTAssertNotNil(expectedImage) + } + + func testAcceptButtonHasBackgroundColor() { + + // Check that the accept button has the correct background color + let expectedColor = NCBrandColor.shared.brand + XCTAssertEqual(viewController?.acceptButton.backgroundColor, expectedColor) + + } + + func testShowSaveSettingsButton() { + + privacySettingsView.isShowSettingsButton = UserDefaults.standard.bool(forKey: "showSettingsButton") + + XCTAssertTrue(privacySettingsView.isShowSettingsButton) + + } + + func testRequiredDataCollectionSectionExists() { + let form : XLFormDescriptor = XLFormDescriptor() as XLFormDescriptor + form.rowNavigationOptions = XLFormRowNavigationOptions.stopDisableRow + + var section : XLFormSectionDescriptor + var row : XLFormRowDescriptor + + // the section with the title "Required Data Collection" + row = XLFormRowDescriptor(tag: "ButtonDestinationFolder", rowType: "RequiredDataCollectionCustomCellType", title: "") + section = XLFormSectionDescriptor.formSection(withTitle: "") + section.footerTitle = NSLocalizedString("_required_data_collection_help_text_", comment: "") + + // Verify that section was found + XCTAssertNotNil(row, "Expected 'Required Data Collection' section to exist in form.") + } + + func testAnalysisDataCollectionSection() { + + let form : XLFormDescriptor = XLFormDescriptor() as XLFormDescriptor + form.rowNavigationOptions = XLFormRowNavigationOptions.stopDisableRow + var section : XLFormSectionDescriptor + var row : XLFormRowDescriptor + + // row with tag "AnalysisDataCollectionSwitch" + row = XLFormRowDescriptor(tag: "AnalysisDataCollectionSwitch", rowType: "AnalysisDataCollectionCustomCellType", title: "") + section = XLFormSectionDescriptor.formSection(withTitle: "") + section.footerTitle = NSLocalizedString("_analysis_data_acqusition_help_text_", comment: "") + + // Assert that the row exists + XCTAssertNotNil(row, "Expected row with tag 'AnalysisDataCollectionSwitch' to exist in form.") + + // Verify the switch is off + XCTAssertFalse(UserDefaults.standard.bool(forKey: "isAnalysisDataCollectionSwitchOn"), "Expected isAnalysisDataCollectionSwitchOn to be false.") + } + + +} From 034c85c05b40e38fba9e8004facb11dac98cb96a Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Mon, 4 Dec 2023 12:01:04 +0530 Subject: [PATCH 03/40] NMC 1984 - privacySettingsViewController file removed to avoid conflicts --- .../PrivacySettingsViewController.swift | 133 ------------------ 1 file changed, 133 deletions(-) diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index 3e60dd6212..e69de29bb2 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -1,133 +0,0 @@ -// -// PrivacySettingsViewController.swift -// Nextcloud -// -// Created by A200073704 on 25/04/23. -// Copyright © 2023 Marino Faggiana. All rights reserved. -// - -import Foundation -import AppTrackingTransparency -import AdSupport - -class PrivacySettingsViewController: XLFormViewController{ - - @objc public var isShowSettingsButton: Bool = false - - override func viewDidLoad() { - super.viewDidLoad() - self.title = NSLocalizedString("_privacy_settings_title_", comment: "") - - NotificationCenter.default.addObserver(self, selector: #selector(changeTheming), name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterChangeTheming), object: nil) - - let nib = UINib(nibName: "CustomSectionHeader", bundle: nil) - self.tableView.register(nib, forHeaderFooterViewReuseIdentifier: "customSectionHeader") - isShowSettingsButton = UserDefaults.standard.bool(forKey: "showSettingsButton") - self.navigationController?.navigationBar.tintColor = NCBrandColor.shared.brand - changeTheming() - } - - @objc func changeTheming() { - tableView.backgroundColor = .systemGroupedBackground - tableView.separatorColor = .none - tableView.separatorColor = .clear - tableView.reloadData() - initializeForm() - } - - //MARK: XLForm - func initializeForm() { - let form : XLFormDescriptor = XLFormDescriptor() as XLFormDescriptor - form.rowNavigationOptions = XLFormRowNavigationOptions.stopDisableRow - - var section : XLFormSectionDescriptor - var row : XLFormRowDescriptor - - // Section: Destination Folder - - section = XLFormSectionDescriptor.formSection(withTitle: NSLocalizedString("", comment: "").uppercased()) - section.footerTitle = " " - form.addFormSection(section) - - section = XLFormSectionDescriptor.formSection(withTitle: NSLocalizedString("", comment: "").uppercased()) - section.footerTitle = NSLocalizedString("_privacy_settings_help_text_", comment: "") - form.addFormSection(section) - - //custom cell - section = XLFormSectionDescriptor.formSection(withTitle: "") - section.footerTitle = NSLocalizedString("_required_data_collection_help_text_", comment: "") - form.addFormSection(section) - - XLFormViewController.cellClassesForRowDescriptorTypes()["RequiredDataCollectionCustomCellType"] = RequiredDataCollectionSwitch.self - - row = XLFormRowDescriptor(tag: "ButtonDestinationFolder", rowType: "RequiredDataCollectionCustomCellType", title: "") - row.cellConfig["requiredDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand - row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue - row.cellConfig["cellLabel.font"] = UIFont.systemFont(ofSize: 15.0) - row.cellConfig["cellLabel.textColor"] = UIColor.label //photos - row.cellConfig["cellLabel.text"] = NSLocalizedString("_required_data_collection_", comment: "") - section.addFormRow(row) - - section = XLFormSectionDescriptor.formSection(withTitle: NSLocalizedString("", comment: "").uppercased()) - section.footerTitle = NSLocalizedString("_analysis_data_acqusition_help_text_", comment: "") - form.addFormSection(section) - - XLFormViewController.cellClassesForRowDescriptorTypes()["AnalysisDataCollectionCustomCellType"] = AnalysisDataCollectionSwitch.self - - row = XLFormRowDescriptor(tag: "AnalysisDataCollectionSwitch", rowType: "AnalysisDataCollectionCustomCellType", title: "") - row.cellConfig["analysisDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand - row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue - row.cellConfig["cellLabel.font"] = UIFont.systemFont(ofSize: 15.0) - row.cellConfig["cellLabel.textColor"] = UIColor.label //photos - row.cellConfig["cellLabel.text"] = NSLocalizedString("_analysis_data_acqusition_", comment: "") - if(UserDefaults.standard.bool(forKey: "isAnalysisDataCollectionSwitchOn")){ - row.cellConfigAtConfigure["analysisDataCollectionSwitchControl.on"] = 1 - }else { - row.cellConfigAtConfigure["analysisDataCollectionSwitchControl.on"] = 0 - } - - section.addFormRow(row) - - XLFormViewController.cellClassesForRowDescriptorTypes()["SaveSettingsButton"] = SaveSettingsCustomButtonCell.self - - section = XLFormSectionDescriptor.formSection(withTitle: "") - form.addFormSection(section) - - row = XLFormRowDescriptor(tag: "SaveSettingsButton", rowType: "SaveSettingsButton", title: "") - row.cellConfig["backgroundColor"] = UIColor.clear - - if(isShowSettingsButton){ - section.addFormRow(row) - } - - self.form = form - } - - override func formRowDescriptorValueHasChanged(_ formRow: XLFormRowDescriptor!, oldValue: Any!, newValue: Any!) { - super.formRowDescriptorValueHasChanged(formRow, oldValue: oldValue, newValue: newValue) - - if formRow.tag == "SaveSettingsButton" { - print("save settings clicked") - //TODO save button state and leave the page - self.navigationController?.popViewController(animated: true) - - } - if formRow.tag == "AnalysisDataCollectionSwitch"{ - if (formRow.value! as AnyObject).boolValue { - if #available(iOS 14, *) { - ATTrackingManager.requestTrackingAuthorization(completionHandler: { (status) in - if status == .denied { - guard let url = URL(string: UIApplication.openSettingsURLString) else { - return - } - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:]) - } - } - }) - } - } - UserDefaults.standard.set((formRow.value! as AnyObject).boolValue, forKey: "isAnalysisDataCollectionSwitchOn") - } - } -} From 13840c7764180b6437d07e40f380abfe05bdd5fc Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Tue, 26 Dec 2023 13:29:37 +0530 Subject: [PATCH 04/40] NMC 1984 - Privacy setting view controller added --- .../PrivacySettingsViewController.swift | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index e69de29bb2..b7883ad800 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -0,0 +1,147 @@ +// +// PrivacySettingsViewController.swift +// Nextcloud +// +// Created by A200073704 on 25/04/23. +// Copyright © 2023 Marino Faggiana. All rights reserved. +// + +import Foundation +import AppTrackingTransparency +import AdSupport + +class PrivacySettingsViewController: XLFormViewController{ + + @objc public var isShowSettingsButton: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + self.title = NSLocalizedString("_privacy_settings_title_", comment: "") + + NotificationCenter.default.addObserver(self, selector: #selector(changeTheming), name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterChangeTheming), object: nil) + + let nib = UINib(nibName: "CustomSectionHeader", bundle: nil) + self.tableView.register(nib, forHeaderFooterViewReuseIdentifier: "customSectionHeader") + isShowSettingsButton = UserDefaults.standard.bool(forKey: "showSettingsButton") + self.navigationController?.navigationBar.tintColor = NCBrandColor.shared.brand + changeTheming() + } + + @objc func changeTheming() { + tableView.backgroundColor = .systemGroupedBackground + tableView.separatorColor = .none + tableView.separatorColor = .clear + tableView.reloadData() + initializeForm() + } + + + + //MARK: XLForm + + func initializeForm() { + + let form : XLFormDescriptor = XLFormDescriptor() as XLFormDescriptor + form.rowNavigationOptions = XLFormRowNavigationOptions.stopDisableRow + + var section : XLFormSectionDescriptor + var row : XLFormRowDescriptor + + // Section: Destination Folder + + section = XLFormSectionDescriptor.formSection(withTitle: NSLocalizedString("", comment: "").uppercased()) + section.footerTitle = " " + form.addFormSection(section) + + section = XLFormSectionDescriptor.formSection(withTitle: NSLocalizedString("", comment: "").uppercased()) + section.footerTitle = NSLocalizedString("_privacy_settings_help_text_", comment: "") + form.addFormSection(section) + + + //custom cell + section = XLFormSectionDescriptor.formSection(withTitle: "") + section.footerTitle = NSLocalizedString("_required_data_collection_help_text_", comment: "") + form.addFormSection(section) + + + XLFormViewController.cellClassesForRowDescriptorTypes()["RequiredDataCollectionCustomCellType"] = RequiredDataCollectionSwitch.self + + + row = XLFormRowDescriptor(tag: "ButtonDestinationFolder", rowType: "RequiredDataCollectionCustomCellType", title: "") + row.cellConfig["requiredDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand + row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue + row.cellConfig["cellLabel.font"] = UIFont.systemFont(ofSize: 15.0) + row.cellConfig["cellLabel.textColor"] = UIColor.label //photos + row.cellConfig["cellLabel.text"] = NSLocalizedString("_required_data_collection_", comment: "") + section.addFormRow(row) + + section = XLFormSectionDescriptor.formSection(withTitle: NSLocalizedString("", comment: "").uppercased()) + section.footerTitle = NSLocalizedString("_analysis_data_acqusition_help_text_", comment: "") + form.addFormSection(section) + + XLFormViewController.cellClassesForRowDescriptorTypes()["AnalysisDataCollectionCustomCellType"] = AnalysisDataCollectionSwitch.self + + + row = XLFormRowDescriptor(tag: "AnalysisDataCollectionSwitch", rowType: "AnalysisDataCollectionCustomCellType", title: "") + row.cellConfig["analysisDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand + row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue + row.cellConfig["cellLabel.font"] = UIFont.systemFont(ofSize: 15.0) + row.cellConfig["cellLabel.textColor"] = UIColor.label //photos + row.cellConfig["cellLabel.text"] = NSLocalizedString("_analysis_data_acqusition_", comment: "") + if(UserDefaults.standard.bool(forKey: "isAnalysisDataCollectionSwitchOn")){ + row.cellConfigAtConfigure["analysisDataCollectionSwitchControl.on"] = 1 + }else { + row.cellConfigAtConfigure["analysisDataCollectionSwitchControl.on"] = 0 + } + + section.addFormRow(row) + + + XLFormViewController.cellClassesForRowDescriptorTypes()["SaveSettingsButton"] = SaveSettingsCustomButtonCell.self + + section = XLFormSectionDescriptor.formSection(withTitle: "") + form.addFormSection(section) + + + row = XLFormRowDescriptor(tag: "SaveSettingsButton", rowType: "SaveSettingsButton", title: "") + row.cellConfig["backgroundColor"] = UIColor.clear + + if(isShowSettingsButton){ + section.addFormRow(row) + } + + + self.form = form + } + + + override func formRowDescriptorValueHasChanged(_ formRow: XLFormRowDescriptor!, oldValue: Any!, newValue: Any!) { + super.formRowDescriptorValueHasChanged(formRow, oldValue: oldValue, newValue: newValue) + + if formRow.tag == "SaveSettingsButton" { + print("save settings clicked") + //TODO save button state and leave the page + self.navigationController?.popViewController(animated: true) + + } + if formRow.tag == "AnalysisDataCollectionSwitch"{ + if (formRow.value! as AnyObject).boolValue { + if #available(iOS 14, *) { + ATTrackingManager.requestTrackingAuthorization(completionHandler: { (status) in + if status == .denied { + guard let url = URL(string: UIApplication.openSettingsURLString) else { + return + } + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:]) + } + } + }) + } + } + UserDefaults.standard.set((formRow.value! as AnyObject).boolValue, forKey: "isAnalysisDataCollectionSwitchOn") + } + + } + +} From 3665ae64ae3724727b3ef2c7bc49e46cd7a4619f Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Wed, 9 Apr 2025 14:10:39 +0530 Subject: [PATCH 05/40] NMC 1984 - Privacy policy customisation changes added --- Nextcloud.xcodeproj/project.pbxproj | 49 ++ ...mon+CollectionViewDelegateFlowLayout.swift | 43 ++ .../NCCollectionViewCommon.swift | 691 ++++++++++++++++-- 3 files changed, 735 insertions(+), 48 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index a3788d3efc..73057bbf53 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -85,6 +85,20 @@ AFCE353727E4ED7B00FEA6C2 /* NCShareCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353627E4ED7B00FEA6C2 /* NCShareCells.swift */; }; AFCE353927E5DE0500FEA6C2 /* Shareable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353827E5DE0400FEA6C2 /* Shareable.swift */; }; CB3666201AF7550816B5CD6A /* NCContextMenuComment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8932E90EC4278026D86CCCC9 /* NCContextMenuComment.swift */; }; + AFCE353927E5DE0500FEA6C2 /* NCShare+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353827E5DE0400FEA6C2 /* NCShare+Helper.swift */; }; + B54315362DA64EB100981E7E /* PrivacyPolicyTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54315352DA64EB100981E7E /* PrivacyPolicyTest.swift */; }; + B54315412DA669C700981E7E /* InitialPrivacySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54315392DA669C700981E7E /* InitialPrivacySettingsViewController.swift */; }; + B54315422DA669C700981E7E /* RequiredDataCollectionSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = B543153D2DA669C700981E7E /* RequiredDataCollectionSwitch.swift */; }; + B54315432DA669C700981E7E /* PrivacySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B543153C2DA669C700981E7E /* PrivacySettingsViewController.swift */; }; + B54315442DA669C700981E7E /* AnalysisDataCollectionSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54315372DA669C700981E7E /* AnalysisDataCollectionSwitch.swift */; }; + B54315452DA669C700981E7E /* PrivacyPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B543153B2DA669C700981E7E /* PrivacyPolicyViewController.swift */; }; + B54315462DA669C700981E7E /* SaveSettingsCustomButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B543153F2DA669C700981E7E /* SaveSettingsCustomButtonCell.swift */; }; + B54315472DA669C700981E7E /* AnalysisDataCollectionSwitch.xib in Resources */ = {isa = PBXBuildFile; fileRef = B54315382DA669C700981E7E /* AnalysisDataCollectionSwitch.xib */; }; + B54315482DA669C700981E7E /* NCSettings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B543153A2DA669C700981E7E /* NCSettings.storyboard */; }; + B54315492DA669C700981E7E /* SaveSettingsCustomButtonCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B54315402DA669C700981E7E /* SaveSettingsCustomButtonCell.xib */; }; + B543154A2DA669C700981E7E /* RequiredDataCollectionSwitch.xib in Resources */ = {isa = PBXBuildFile; fileRef = B543153E2DA669C700981E7E /* RequiredDataCollectionSwitch.xib */; }; + C04E2F232A17BB4D001BAD85 /* FilesIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C04E2F222A17BB4D001BAD85 /* FilesIntegrationTests.swift */; }; + D575039F27146F93008DC9DC /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A0D1342591FBC5008F8A13 /* String+Extension.swift */; }; D5B6AA7827200C7200D49C24 /* NCActivityTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B6AA7727200C7200D49C24 /* NCActivityTableViewCell.swift */; }; F310B1EF2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = F310B1EE2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift */; }; F31165022F9674A1009A1E37 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = F31165012F9674A1009A1E37 /* AppIcon.icon */; }; @@ -1270,6 +1284,18 @@ AFCE353827E5DE0400FEA6C2 /* Shareable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shareable.swift; sourceTree = ""; }; B4C7A5B36D1ED178FB6B76CB /* NCContextMenuPlayerTracks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenuPlayerTracks.swift; sourceTree = ""; }; BB7697C94BA14450A0867940 /* NCContextMenuProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenuProfile.swift; sourceTree = ""; }; + AFCE353827E5DE0400FEA6C2 /* NCShare+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShare+Helper.swift"; sourceTree = ""; }; + B54315352DA64EB100981E7E /* PrivacyPolicyTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyTest.swift; sourceTree = ""; }; + B54315372DA669C700981E7E /* AnalysisDataCollectionSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalysisDataCollectionSwitch.swift; sourceTree = ""; }; + B54315382DA669C700981E7E /* AnalysisDataCollectionSwitch.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AnalysisDataCollectionSwitch.xib; sourceTree = ""; }; + B54315392DA669C700981E7E /* InitialPrivacySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialPrivacySettingsViewController.swift; sourceTree = ""; }; + B543153A2DA669C700981E7E /* NCSettings.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCSettings.storyboard; sourceTree = ""; }; + B543153B2DA669C700981E7E /* PrivacyPolicyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyViewController.swift; sourceTree = ""; }; + B543153C2DA669C700981E7E /* PrivacySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsViewController.swift; sourceTree = ""; }; + B543153D2DA669C700981E7E /* RequiredDataCollectionSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequiredDataCollectionSwitch.swift; sourceTree = ""; }; + B543153E2DA669C700981E7E /* RequiredDataCollectionSwitch.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RequiredDataCollectionSwitch.xib; sourceTree = ""; }; + B543153F2DA669C700981E7E /* SaveSettingsCustomButtonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveSettingsCustomButtonCell.swift; sourceTree = ""; }; + B54315402DA669C700981E7E /* SaveSettingsCustomButtonCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SaveSettingsCustomButtonCell.xib; sourceTree = ""; }; C0046CDA2A17B98400D87C9D /* NextcloudUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C04E2F202A17BB4D001BAD85 /* NextcloudIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D5B6AA7727200C7200D49C24 /* NCActivityTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCActivityTableViewCell.swift; sourceTree = ""; }; @@ -2094,6 +2120,8 @@ children = ( F34BDB3B2F574A58007A222C /* BidiSafeFilenameTests.swift */, AA52EB452D42AC5A0089C348 /* Placeholder.swift */, + B54315352DA64EB100981E7E /* PrivacyPolicyTest.swift */, + AF8ED1FB2757821000B8DBC4 /* NextcloudUnitTests.swift */, ); path = NextcloudUnitTests; sourceTree = ""; @@ -2611,6 +2639,16 @@ F76882042C0DD1E7001CF441 /* Settings */ = { isa = PBXGroup; children = ( + B54315372DA669C700981E7E /* AnalysisDataCollectionSwitch.swift */, + B54315382DA669C700981E7E /* AnalysisDataCollectionSwitch.xib */, + B54315392DA669C700981E7E /* InitialPrivacySettingsViewController.swift */, + B543153A2DA669C700981E7E /* NCSettings.storyboard */, + B543153B2DA669C700981E7E /* PrivacyPolicyViewController.swift */, + B543153C2DA669C700981E7E /* PrivacySettingsViewController.swift */, + B543153D2DA669C700981E7E /* RequiredDataCollectionSwitch.swift */, + B543153E2DA669C700981E7E /* RequiredDataCollectionSwitch.xib */, + B543153F2DA669C700981E7E /* SaveSettingsCustomButtonCell.swift */, + B54315402DA669C700981E7E /* SaveSettingsCustomButtonCell.xib */, F768820B2C0DD1E7001CF441 /* Settings */, F76882162C0DD1E7001CF441 /* AutoUpload */, F389C9F32CEE381E00049762 /* SelectAlbum */, @@ -4051,6 +4089,11 @@ F7CEE6002BA9A5C9003EFD89 /* NCTrashGridCell.xib in Resources */, AF93471C27E2361E002537EE /* NCShareHeader.xib in Resources */, F75D901F2D2BE12E003E740B /* NCRecommendationsCell.xib in Resources */, + B54315472DA669C700981E7E /* AnalysisDataCollectionSwitch.xib in Resources */, + B54315482DA669C700981E7E /* NCSettings.storyboard in Resources */, + B54315492DA669C700981E7E /* SaveSettingsCustomButtonCell.xib in Resources */, + B543154A2DA669C700981E7E /* RequiredDataCollectionSwitch.xib in Resources */, + F76032A0252F0F8E0015A421 /* NCTransferCell.xib in Resources */, F7F4F10527ECDBDB008676F9 /* Inconsolata-SemiBold.ttf in Resources */, F7A48415297028FC00BD1B49 /* Nextcloud Hub.png in Resources */, F74C0437253F1CDC009762AB /* NCShares.storyboard in Resources */, @@ -4621,6 +4664,12 @@ F72408332B8A27C900F128E2 /* NCMedia+Command.swift in Sources */, F755CB402B8CB13C00CE27E9 /* NCMediaLayout.swift in Sources */, F73EF7B72B0224AB0087E6E9 /* NCManageDatabase+ExternalSites.swift in Sources */, + B54315412DA669C700981E7E /* InitialPrivacySettingsViewController.swift in Sources */, + B54315422DA669C700981E7E /* RequiredDataCollectionSwitch.swift in Sources */, + B54315432DA669C700981E7E /* PrivacySettingsViewController.swift in Sources */, + B54315442DA669C700981E7E /* AnalysisDataCollectionSwitch.swift in Sources */, + B54315452DA669C700981E7E /* PrivacyPolicyViewController.swift in Sources */, + B54315462DA669C700981E7E /* SaveSettingsCustomButtonCell.swift in Sources */, AF4BF61927562A4B0081CEEF /* NCManageDatabase+Metadata.swift in Sources */, F78A18B623CDD07D00F681F3 /* NCViewerRichWorkspaceWebView.swift in Sources */, AFA2AC8527849604008E1EA7 /* NCActivityCommentView.swift in Sources */, diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift index 06795639bc..42bbbd6038 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift @@ -13,4 +13,47 @@ extension NCCollectionViewCommon: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { return sizeForFooterInSection(section: section) } + + func getHeaderHeight() -> CGFloat { + + var size: CGFloat = 0 + // transfer in progress + if headerMenuTransferView, + let metadata = NCManageDatabase.shared.getMetadataFromOcId(NCNetworking.shared.transferInForegorund?.ocId), + metadata.isTransferInForeground { + if !isSearchingMode { + size += NCGlobal.shared.heightHeaderTransfer + } + } else { + NCNetworking.shared.transferInForegorund = nil + } + + if headerMenuButtonsView { + size += NCGlobal.shared.heightButtonsView + } + + return size + } + + func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { + + var headerRichWorkspace: CGFloat = 0 + + if let richWorkspaceText = richWorkspaceText, showDescription { + let trimmed = richWorkspaceText.trimmingCharacters(in: .whitespaces) + if !trimmed.isEmpty && !isSearchingMode { + headerRichWorkspace = UIScreen.main.bounds.size.height / 6 + } + } + + if isSearchingMode || layoutForView?.layout == NCGlobal.shared.layoutGrid || dataSource.numberOfSections() > 1 { + if section == 0 { + return (getHeaderHeight(), headerRichWorkspace, NCGlobal.shared.heightSection) + } else { + return (0, 0, NCGlobal.shared.heightSection) + } + } else { + return (getHeaderHeight(), headerRichWorkspace, 0) + } + } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index af4f6d4ff4..5ce5eb5132 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -10,6 +10,7 @@ import EasyTipView import LucidBanner class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { +//class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionHeaderMenuDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate, NCEmptyDataSetDelegate { @IBOutlet weak var collectionView: UICollectionView! @@ -83,6 +84,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, internal var emptyImageColors: [UIColor]? internal var emptyTitle: String = "" + let maxImageGrid: CGFloat = 7 + var headerMenu: NCSectionHeaderMenu? + var headerMenuTransferView = false + var headerMenuButtonsView: Bool = true + var headerRichWorkspaceDisable: Bool = false + + var groupByField = "name" + + var emptyImage: UIImage? + internal var emptyDescription: String = "" internal var emptyDataPortaitOffset: CGFloat = 0 internal var emptyDataLandscapeOffset: CGFloat = -20 @@ -99,8 +110,13 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, internal var numberOfColumns: Int = 0 internal var lastNumberOfColumns: Int = 0 - internal let heightHeaderRecommendations: CGFloat = 160 - internal let heightHeaderSection: CGFloat = 30 + var isTransitioning: Bool = false + var selectableDataSource: [RealmSwiftObject] { dataSource.getMetadataSourceForAllSections() } + var pushed: Bool = false + var emptyDataSet: NCEmptyDataSet? + + let heightHeaderRecommendations: CGFloat = 160 + let heightHeaderSection: CGFloat = 30 internal var isLayoutPhoto: Bool { layoutForView?.layout == global.layoutPhotoRatio || layoutForView?.layout == global.layoutPhotoSquare @@ -195,7 +211,9 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, collectionView.backgroundColor = .systemBackground refreshControl.tintColor = .clear definesPresentationContext = true - + listLayout = NCListLayout() + gridLayout = NCGridLayout() + if enableSearchBar { searchController = UISearchController(searchResultsController: nil) searchController?.searchResultsUpdater = self @@ -209,6 +227,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, navigationItem.searchController = searchController navigationItem.hidesSearchBarWhenScrolling = false navigationItem.preferredSearchBarPlacement = .inline + navigationItem.backBarButtonItem = UIBarButtonItem(title: NSLocalizedString("_back_", comment: ""), style: .plain, target: nil, action: nil) } let interaction = UIEditMenuInteraction(delegate: self) @@ -222,12 +241,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, collectionView.register(UINib(nibName: "NCTransferCell", bundle: nil), forCellWithReuseIdentifier: "transferCell") // Header - collectionView.register(UINib(nibName: "NCSectionFirstHeader", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "sectionFirstHeader") - collectionView.register(UINib(nibName: "NCSectionFirstHeader", bundle: nil), forSupplementaryViewOfKind: mediaSectionHeader, withReuseIdentifier: "sectionFirstHeader") + collectionView.register(UINib(nibName: "NCSectionHeaderMenu", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "sectionHeaderMenu") collectionView.register(UINib(nibName: "NCSectionHeader", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "sectionHeader") - collectionView.register(UINib(nibName: "NCSectionHeader", bundle: nil), forSupplementaryViewOfKind: mediaSectionHeader, withReuseIdentifier: "sectionHeader") - collectionView.register(UINib(nibName: "NCSectionFirstHeaderEmptyData", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "sectionFirstHeaderEmptyData") - collectionView.register(UINib(nibName: "NCSectionFirstHeaderEmptyData", bundle: nil), forSupplementaryViewOfKind: mediaSectionHeader, withReuseIdentifier: "sectionFirstHeaderEmptyData") // Footer collectionView.register(UINib(nibName: "NCSectionFooter", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "sectionFooter") @@ -247,6 +262,9 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.mainNavigationController?.menuPlus?.resetPlusButtonAlpha() } } + + // Empty + emptyDataSet = NCEmptyDataSet(view: collectionView, offset: getHeaderHeight(), delegate: self) let longPressedGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPressCollecationView(_:))) longPressedGesture.minimumPressDuration = 0.5 @@ -323,6 +341,11 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, navigationController?.navigationBar.topItem?.title = titlePreviusFolder } navigationItem.title = titleCurrentFolder + navigationController?.setNavigationBarAppearance() + navigationController?.navigationBar.prefersLargeTitles = true + navigationController?.setNavigationBarHidden(false, animated: true) + + appDelegate.activeViewController = self if tabBarSelect == nil { tabBarSelect = NCCollectionViewCommonSelectTabBar(controller: self.controller, viewController: self, delegate: self) @@ -333,11 +356,15 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, Task { await NCNetworking.shared.transferDispatcher.addDelegate(self) - await (self.navigationController as? NCMainNavigationController)?.setNavigationLeftItems() +// await (self.navigationController as? NCMainNavigationController)?.setNavigationLeftItems() await (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() } + /// Magentacloud branding changes hide user account button on left navigation bar +// setNavigationLeftItems() +// setNavigationRightItems() layoutForView = database.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) + gridLayout.column = CGFloat(layoutForView?.columnGrid ?? 3) if isLayoutList { collectionView?.collectionViewLayout = listLayout self.layoutType = global.layoutList @@ -360,12 +387,37 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive(_:)), name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(closeRichWorkspaceWebView), name: NSNotification.Name(rawValue: global.notificationCenterCloseRichWorkspaceWebView), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(changeStatusFolderE2EE(_:)), name: NSNotification.Name(rawValue: global.notificationCenterChangeStatusFolderE2EE), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(changeLayout(_:)), name: NSNotification.Name(rawValue: global.notificationCenterChangeLayout), object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(deleteFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterDeleteFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(copyMoveFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterCopyMoveFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(renameFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterRenameFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(createFolder(_:)), name: NSNotification.Name(rawValue: global.notificationCenterCreateFolder), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(favoriteFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterFavoriteFile), object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(downloadStartFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterDownloadStartFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(downloadedFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterDownloadedFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(downloadCancelFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterDownloadCancelFile), object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(uploadStartFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterUploadStartFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(uploadedFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterUploadedFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(uploadedLivePhoto(_:)), name: NSNotification.Name(rawValue: global.notificationCenterUploadedLivePhoto), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(uploadCancelFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterUploadCancelFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(updateShare(_:)), name: NSNotification.Name(rawValue: global.notificationCenterUpdateShare), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(triggerProgressTask(_:)), name: NSNotification.Name(rawValue: global.notificationCenterProgressTask), object: nil) + + // FIXME: iPAD PDF landscape mode iOS 16 + DispatchQueue.main.async { + self.collectionView?.collectionViewLayout.invalidateLayout() + } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) dismissTip() - + pushed = false + toggleSelect(isOn: false) // Cancel Queue & Retrieves Properties self.networking.downloadThumbnailQueue.cancelAll() Task { @@ -382,17 +434,38 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCloseRichWorkspaceWebView), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterChangeStatusFolderE2EE), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterChangeLayout), object: nil) + + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDeleteFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCopyMoveFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCopyFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterMoveFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterRenameFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCreateFolder), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterFavoriteFile), object: nil) + + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDownloadStartFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDownloadedFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDownloadCancelFile), object: nil) + + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterUploadStartFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterUploadedFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterUploadedLivePhoto), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterUploadCancelFile), object: nil) + + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterUpdateShare), object: nil) removeImageCache(metadatas: self.dataSource.getMetadatas()) } - func isApplicationUpdated() -> Bool{ + func isApplicationUpdated() -> Bool { let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") return currentVersion != appVersion } - func redirectToPrivacyViewController(){ + func redirectToPrivacyViewController() { let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController newViewController.modalPresentationStyle = .fullScreen @@ -462,6 +535,9 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.present(alertController, animated: true) } + self.collectionView.collectionViewLayout.invalidateLayout() + +// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() } internal func setLayout(layoutForView: NCDBLayoutForView, withSubFolders: Bool = false) async { @@ -485,6 +561,347 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, collectionView.collectionViewLayout.invalidateLayout() } + @objc func reloadDataSource(_ notification: NSNotification) { + if let userInfo = notification.userInfo as? NSDictionary { + if let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } + } + + if let clearDataSource = userInfo["clearDataSource"] as? Bool, clearDataSource { + self.dataSource.removeAll() + } + } + + reloadDataSource() + } + + @objc func getServerData(_ notification: NSNotification) { + if let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } + } + + getServerData() + } + + @objc func reloadHeader(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + account == session.account + else { return } + + self.collectionView.reloadData() + } + + @objc func changeStatusFolderE2EE(_ notification: NSNotification) { + reloadDataSource() + } + + @objc func closeRichWorkspaceWebView() { + reloadDataSource() + } + + @objc func deleteFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError else { return } + + if error == .success { + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + } else { + NCContentPresenter().showError(error: error) + } + + reloadDataSource() + } + + @objc func copyMoveFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + account == session.account else { return } + + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + + if serverUrl == self.serverUrl { + reloadDataSource() + } + } + + @objc func renameFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let error = userInfo["error"] as? NKError, + account == session.account + else { return } + + if error == .success { + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + } + + if serverUrl == self.serverUrl { + if error != .success { + NCContentPresenter().showError(error: error) + } + reloadDataSource() + } else { + collectionView.reloadData() + } + } + + @objc func createFolder(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let account = userInfo["account"] as? String, + account == session.account, + let withPush = userInfo["withPush"] as? Bool, + let metadata = database.getMetadataFromOcId(ocId) + else { return } + + if isSearchingMode { + return networkSearch() + } + + if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { + reloadDataSource() + } else if withPush, metadata.serverUrl == self.serverUrl { + reloadDataSource() + if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { + if sceneIdentifier == controller?.sceneIdentifier { + pushMetadata(metadata) + } + } else { + pushMetadata(metadata) + } + } + } + + @objc func favoriteFile(_ notification: NSNotification) { + if isSearchingMode { + return networkSearch() + } + + if self is NCFavorite { + return reloadDataSource() + } + + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + serverUrl == self.serverUrl + else { return } + + reloadDataSource() + } + + @objc func downloadStartFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func downloadedFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func downloadCancelFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadStartFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + !isSearchingMode, + let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) + else { return } + + // Header view trasfer + if metadata.isTransferInForeground { + NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) + DispatchQueue.main.async { self.collectionView?.reloadData() } + } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadedFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadedLivePhoto(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadCancelFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func updateShare(_ notification: NSNotification) { + if isSearchingMode { + networkSearch() + } else { + self.dataSource.removeAll() + getServerData() + } + } + + @objc func triggerProgressTask(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let progressNumber = userInfo["progress"] as? NSNumber, + let totalBytes = userInfo["totalBytes"] as? Int64, + let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, + let ocId = userInfo["ocId"] as? String, + let ocIdTransfer = userInfo["ocIdTransfer"] as? String, + let session = userInfo["session"] as? String + else { return } + + let chunk: Int = userInfo["chunk"] as? Int ?? 0 + let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false + + let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) + + // HEADER +// if self.headerMenuTransferView, transfer.session.contains("upload") { +// self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) +// self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) +// } + + DispatchQueue.main.async { + if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { + if NCNetworking.shared.transferInForegorund?.ocId == ocId { + NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue + } else { + NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) + self.collectionView.reloadData() + } + self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue + } else { + guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, + let cell = self.collectionView?.cellForItem(at: indexPath), + let cell = cell as? NCCellProtocol else { return } + if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { + cell.fileProgressView?.isHidden = true + cell.fileProgressView?.progress = .zero + cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) + if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { + cell.writeInfoDateSize(date: metadata.date, size: metadata.size) + } else { + cell.fileInfoLabel?.text = "" + cell.fileSubinfoLabel?.text = "" + } + } else { + cell.fileProgressView?.isHidden = false + cell.fileProgressView?.progress = progressNumber.floatValue + cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) + let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal + if status == NCGlobal.shared.metadataStatusDownloading { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) + } else if status == NCGlobal.shared.metadataStatusUploading { + if totalBytes > 0 { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) + } else { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" + } + } + } + } + } + } + + // MARK: - Layout + + func setNavigationLeftItems() { + navigationItem.title = titleCurrentFolder + } + func getNavigationTitle() -> String { let tblAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) if let tblAccount, @@ -527,6 +944,36 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.navigationItem.title = self.titleCurrentFolder } + // MARK: - Empty + + func emptyDataSetView(_ view: NCEmptyView) { + + self.emptyDataSet?.setOffset(getHeaderHeight()) + if isSearchingMode { + view.emptyImage.image = UIImage(named: "search")?.image(color: .gray, size: UIScreen.main.bounds.width) + if self.dataSourceTask?.state == .running { + view.emptyTitle.text = NSLocalizedString("_search_in_progress_", comment: "") + } else { + view.emptyTitle.text = NSLocalizedString("_search_no_record_found_", comment: "") + } + view.emptyDescription.text = NSLocalizedString("_search_instruction_", comment: "") + } else if self.dataSourceTask?.state == .running { + view.emptyImage.image = UIImage(named: "networkInProgress")?.image(color: .gray, size: UIScreen.main.bounds.width) + view.emptyTitle.text = NSLocalizedString("_request_in_progress_", comment: "") + view.emptyDescription.text = "" + } else { + if serverUrl.isEmpty { + view.emptyImage.image = emptyImage + view.emptyTitle.text = NSLocalizedString(emptyTitle, comment: "") + view.emptyDescription.text = NSLocalizedString(emptyDescription, comment: "") + } else { + view.emptyImage.image = UIImage(named: "folder_nmcloud") + view.emptyTitle.text = NSLocalizedString("_files_no_files_", comment: "") + view.emptyDescription.text = NSLocalizedString("_no_file_pull_down_", comment: "") + } + } + } + // MARK: - SEARCH func searchController(enabled: Bool) { @@ -566,6 +1013,68 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, Task { await self.search() } +// if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { +// networkSearch() + } + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + self.networking.cancelUnifiedSearchFiles() + + self.isSearchingMode = false + self.literalSearch = "" + self.providers?.removeAll() + self.dataSource.removeAll() + Task { + await self.reloadDataSource() + } + // + mainNavigationController?.hiddenPlusButton(false) + } + + // MARK: - TAP EVENT + + func tapMoreListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) + } + + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) + } + + func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) { + if isEditMode { return } + guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } + + NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) +// NCDownloadAction.shared.openShare(viewController: self, metadata: metadata, page: .sharing) + TealiumHelper.shared.trackEvent(title: "magentacloud-app.filebrowser.sharing", data: ["": ""]) + appDelegate.adjust.trackEvent(TriggerEvent(Sharing.rawValue)) + } + + func tapMoreGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { + if isEditMode { return } + guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } +// toggleMenu(metadata: metadata, image: image) + if namedButtonMore == NCGlobal.shared.buttonMoreMore || namedButtonMore == NCGlobal.shared.buttonMoreLock { + toggleMenu(metadata: metadata, image: image) + } else if namedButtonMore == NCGlobal.shared.buttonMoreStop { + Task { + await cancelSession(metadata: metadata) + } + } + } + + func tapRichWorkspace(_ sender: Any) { + if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { + if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { + viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" + viewerRichWorkspace.serverUrl = serverUrl + viewerRichWorkspace.delegate = self + + navigationController.modalPresentationStyle = .fullScreen + self.present(navigationController, animated: true, completion: nil) + } } } @@ -612,6 +1121,54 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // MARK: - TAP EVENT + func tapRecommendations(with metadata: tableMetadata) { + didSelectMetadata(metadata, withOcIds: false) + } + + func tapButtonSwitch(_ sender: Any) { + guard !isTransitioning else { return } + isTransitioning = true + + guard let layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) else { return } + + if layoutForView.layout == NCGlobal.shared.layoutGrid { + layoutForView.layout = NCGlobal.shared.layoutList + } else { + layoutForView.layout = NCGlobal.shared.layoutGrid + } + self.layoutForView = NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) + self.collectionView.reloadData() + self.collectionView.collectionViewLayout.invalidateLayout() + self.collectionView.setCollectionViewLayout(layoutForView.layout == NCGlobal.shared.layoutList ? self.listLayout : self.gridLayout, animated: true) {_ in self.isTransitioning = false } + } + + func tapButtonOrder(_ sender: Any) { + +// if let titleButtonHeader = NCKeychain().getTitleButtonHeader(account: session.account), !titleButtonHeader.isEmpty { +// layoutForView?.titleButtonHeader = titleButtonHeader +// } +// NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView!) + + let sortMenu = NCSortMenu() + sortMenu.toggleMenu(viewController: self, account: session.account, key: layoutKey, sortButton: sender as? UIButton, serverUrl: serverUrl) + } + + func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func longPressListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func tapShareListItem(with objectId: String, indexPath: IndexPath, sender: Any) { } + + func longPressMoreListItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func longPressGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func longPressMoreGridItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func tapButtonTransfer(_ sender: Any) { } + + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } + @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) } @@ -626,11 +1183,27 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // MARK: - DataSource - @MainActor - func reloadDataSource() async { - if !isSearchingMode { - Task.detached { - if await self.isRecommendationActived() { + @objc func reloadDataSource() async { + + // get auto upload folder + autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() + autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: session.urlBase, userId: session.userId, account: session.account) + + // get layout for view + layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) + // set GroupField for Grid + if !isSearchingMode && layoutForView?.layout == NCGlobal.shared.layoutGrid { + groupByField = "classFile" + } else { + groupByField = "name" + } + + if isSearchingMode { + isDirectoryEncrypted = false + } else { + isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) + if isRecommendationActived { + Task.detached { await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) } } @@ -641,6 +1214,11 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } await mainNavigationController?.updateMenuOption() + (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + self.refreshControl.endRefreshing() + self.collectionView.reloadData() + self.setNavigationRightItems() + } } func getServerData(forced: Bool = false) async { } @@ -714,39 +1292,56 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, navigationController.pushViewController(viewController, animated: true) } - // MARK: - Header size - - func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, - heightHeaderRecommendations: CGFloat, - heightHeaderSection: CGFloat) { - var heightHeaderRichWorkspace: CGFloat = 0 - var heightHeaderRecommendations: CGFloat = 0 - var heightHeaderSection: CGFloat = 0 + func pushViewController(viewController: UIViewController) { + if pushed { return } - if showDescription, - !isSearchingMode, - let richWorkspaceText = self.richWorkspaceText, - !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { - heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 - } - - if isRecommendationActived, - !isSearchingMode, - NCPreferences().showRecommendedFiles, - !self.database.getRecommendedFiles(account: self.session.account).isEmpty { - heightHeaderRecommendations = self.heightHeaderRecommendations - heightHeaderSection = self.heightHeaderSection - } + pushed = true + navigationController?.pushViewController(viewController, animated: true) + } + + // MARK: - Header size - if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { - if section == 0 { - return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) - } else { - return (0, 0, self.heightHeaderSection) - } - } else { - return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) +// func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, +// heightHeaderRecommendations: CGFloat, +// heightHeaderSection: CGFloat) { +// var heightHeaderRichWorkspace: CGFloat = 0 +// var heightHeaderRecommendations: CGFloat = 0 +// var heightHeaderSection: CGFloat = 0 +// +// if showDescription, +// !isSearchingMode, +// let richWorkspaceText = self.richWorkspaceText, +// !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { +// heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 +// } +// +// if isRecommendationActived, +// !isSearchingMode, +// NCKeychain().showRecommendedFiles, +// !self.database.getRecommendedFiles(account: self.session.account).isEmpty { +// heightHeaderRecommendations = self.heightHeaderRecommendations +// heightHeaderSection = self.heightHeaderSection +// } +// +// if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { +// if section == 0 { +// return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) +// } else { +// return (0, 0, self.heightHeaderSection) +// } +// } else { +// return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) +// } +// } + + func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { + if headerMenuTransferView, + NCNetworking.shared.isOnline, + let results = database.getResultsMetadatas(predicate: NSPredicate(format: "status IN %@", [global.metadataStatusWaitUpload, global.metadataStatusUploading])), + !results.isEmpty { + return Array(results) } + return nil } func sizeForHeaderInSection(section: Int) -> CGSize { @@ -755,7 +1350,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let isIphone = UIDevice.current.userInterfaceIdiom == .phone if self.dataSource.isEmpty() { - height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset) + height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset, isHeaderMenuTransferViewEnabled: isHeaderMenuTransferViewEnabled() != nil) } else if isEditMode || (isLandscape && isIphone) { return CGSize.zero } else { From 49ad292f72c8b33a47cd9690365860a4c53354ec Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Tue, 30 Sep 2025 22:00:01 +0530 Subject: [PATCH 06/40] NMC 1984 - Privacy policy customisation added --- .../NCCollectionViewCommon.swift | 666 +++++++++++++----- 1 file changed, 498 insertions(+), 168 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 5ce5eb5132..2e9fbed9f5 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -4,74 +4,64 @@ import UIKit import SwiftUI +import Realm import RealmSwift import NextcloudKit import EasyTipView import LucidBanner +import MoEngageInApps -class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { -//class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionHeaderMenuDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate, NCEmptyDataSetDelegate { +class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionHeaderMenuDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate, NCEmptyDataSetDelegate { @IBOutlet weak var collectionView: UICollectionView! - internal let database = NCManageDatabase.shared - internal let global = NCGlobal.shared - internal let utility = NCUtility() - internal let utilityFileSystem = NCUtilityFileSystem() - internal let imageCache = NCImageCache.shared - internal var dataSource = NCCollectionViewDataSource() - internal let networking = NCNetworking.shared - internal let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! - internal var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() - private var isNavigatingMetadata = false - - internal var autoUploadFileName = "" - internal var autoUploadDirectory = "" - internal let refreshControl = UIRefreshControl() - internal var searchController: UISearchController? - internal var backgroundImageView = UIImageView() - internal var serverUrl: String = "" - internal var isEditMode = false - internal var isDirectoryE2EE = false - internal var fileSelect: [String] = [] - internal var metadataFolder: tableMetadata? - internal var richWorkspaceText: String? - internal var sectionFirstHeader: NCSectionFirstHeader? - internal var sectionFirstHeaderEmptyData: NCSectionFirstHeaderEmptyData? - - // Layout - // - internal var layoutForView: NCDBLayoutForView? - internal var layoutForViewLayoutStore: String? - internal var listLayout = NCListLayout() - internal var gridLayout = NCGridLayout() - internal var mediaLayout = NCMediaLayout() - internal var layoutType = NCGlobal.shared.layoutList - - internal var tabBarSelect: NCCollectionViewCommonSelectTabBar? - - internal var attributesZoomIn: UIMenuElement.Attributes = [] - internal var attributesZoomOut: UIMenuElement.Attributes = [] - - internal var tipViewAccounts: EasyTipView? - internal var syncMetadatasTask: Task? - - // Edit Menu - // - internal let dragDropMenuIdentifier = "dragdrop" - internal var editMenuInteraction: UIEditMenuInteraction? - internal var currentMenuObjectId: String? - internal var currentMenuPoint: CGPoint = .zero - - // Search - // - internal var isSearchingMode: Bool = false - internal var networkSearchInProgress: Bool = false - internal var searchOperationHandle = NKOperationHandle() - internal var searchTask: URLSessionTask? - internal var searchResultText: String? - internal var searchResultStore: String? + let database = NCManageDatabase.shared + let global = NCGlobal.shared + let utility = NCUtility() + let utilityFileSystem = NCUtilityFileSystem() + let imageCache = NCImageCache.shared + var dataSource = NCCollectionViewDataSource() + let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! + var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() + + var autoUploadFileName = "" + var autoUploadDirectory = "" + let refreshControl = UIRefreshControl() + var searchController: UISearchController? + var backgroundImageView = UIImageView() + var serverUrl: String = "" + var isEditMode = false + var isDirectoryEncrypted = false + var fileSelect: [String] = [] + var metadataFolder: tableMetadata? + var richWorkspaceText: String? + var sectionFirstHeader: NCSectionFirstHeader? + var sectionFirstHeaderEmptyData: NCSectionFirstHeaderEmptyData? + var isSearchingMode: Bool = false + var networkSearchInProgress: Bool = false + var layoutForView: NCDBLayoutForView? + var dataSourceTask: URLSessionTask? + var providers: [NKSearchProvider]? + var searchResults: [NKSearchResult]? + var listLayout = NCListLayout() + var gridLayout = NCGridLayout() + var mediaLayout = NCMediaLayout() + var layoutType = NCGlobal.shared.layoutList + var literalSearch: String? + var tabBarSelect: NCCollectionViewCommonSelectTabBar? + var attributesZoomIn: UIMenuElement.Attributes = [] + var attributesZoomOut: UIMenuElement.Attributes = [] + var syncMetadatasTask: Task? + let maxImageGrid: CGFloat = 7 +// var headerMenu: NCSectionFirstHeader? + var tipViewAccounts: EasyTipView? + var tipViewAutoUpload: EasyTipView? + var headerMenu: NCSectionHeaderMenu? + var headerMenuTransferView = false + var headerMenuButtonsView: Bool = true + var headerRichWorkspaceDisable: Bool = false + // DECLARE // internal var layoutKey = "" @@ -118,6 +108,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let heightHeaderRecommendations: CGFloat = 160 let heightHeaderSection: CGFloat = 30 + var session: NCSession.Session { + NCSession.shared.getSession(controller: tabBarController) + } + internal var isLayoutPhoto: Bool { layoutForView?.layout == global.layoutPhotoRatio || layoutForView?.layout == global.layoutPhotoSquare } @@ -163,6 +157,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, (self.tabBarController as? NCMainTabBarController)?.sceneIdentifier ?? "" } + var defaultPredicate: NSPredicate { + let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, NCGlobal.shared.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) + return predicate + } + + var personalFilesOnlyPredicate: NSPredicate { + let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND (ownerId == %@ || ownerId == '') AND mountType == '' AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, session.userId, global.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) + return predicate + } + @MainActor internal var windowScene: UIWindowScene? { SceneManager.shared.getWindowScene(controller: self.tabBarController as? NCMainTabBarController) @@ -203,6 +207,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, override func viewDidLoad() { super.viewDidLoad() + tabBarSelect = NCCollectionViewCommonSelectTabBar(controller: self.controller, delegate: self) self.navigationController?.presentationController?.delegate = self collectionView.alwaysBounceVertical = true collectionView.accessibilityIdentifier = "NCCollectionViewCommon" @@ -260,6 +265,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // Wait 1.5 seconds before resetting the button alpha try? await Task.sleep(for: .seconds(1.5)) self.mainNavigationController?.menuPlus?.resetPlusButtonAlpha() +// self.dataSource.removeAll() +// self.getServerData() +// if self.isRecommendationActived { +// Task.detached { +// await NCNetworking.shared.createRecommendations(session: self.session) +// } +// } +// self.refreshControl.endRefreshing() +// DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { +// self.resetPlusButtonAlpha() } } @@ -310,6 +325,12 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, await self.debouncerReloadData.resume() } } + + NotificationCenter.default.addObserver(self, selector: #selector(changeTheming(_:)), name: NSNotification.Name(rawValue: global.notificationCenterChangeTheming), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(reloadDataSource(_:)), name: NSNotification.Name(rawValue: global.notificationCenterReloadDataSource), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(getServerData(_:)), name: NSNotification.Name(rawValue: global.notificationCenterGetServerData), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(reloadHeader(_:)), name: NSNotification.Name(rawValue: global.notificationCenterReloadHeader), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(updateIcons), name: NSNotification.Name(rawValue: global.notificationCenterUpdateIcons), object: nil) DispatchQueue.main.async { self.collectionView?.collectionViewLayout.invalidateLayout() @@ -337,6 +358,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + AnalyticsHelper.shared.displayInAppNotification() + if titlePreviusFolder != nil { navigationController?.navigationBar.topItem?.title = titlePreviusFolder } @@ -346,11 +369,12 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, navigationController?.setNavigationBarHidden(false, animated: true) appDelegate.activeViewController = self + appDelegate.account = session.account + appDelegate.urlBase = session.urlBase + appDelegate.userId = session.userId + appDelegate.user = session.user - if tabBarSelect == nil { - tabBarSelect = NCCollectionViewCommonSelectTabBar(controller: self.controller, viewController: self, delegate: self) - } - + NCKeychain().setAccountName(account: session.account) isEditMode = false Task { @@ -385,6 +409,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + NCNetworking.shared.transferDelegate = self + NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive(_:)), name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(closeRichWorkspaceWebView), name: NSNotification.Name(rawValue: global.notificationCenterCloseRichWorkspaceWebView), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(changeStatusFolderE2EE(_:)), name: NSNotification.Name(rawValue: global.notificationCenterChangeStatusFolderE2EE), object: nil) @@ -415,6 +441,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + + NCNetworking.shared.cancelUnifiedSearchFiles() dismissTip() pushed = false toggleSelect(isOn: false) @@ -423,14 +451,14 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, Task { await searchOperationHandle.cancel() } + NCNetworking.shared.unifiedSearchQueue.cancelAll() + dataSourceTask?.cancel() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - Task { - await NCNetworking.shared.transferDispatcher.removeDelegate(self) - } + NCNetworking.shared.transferDelegate = nil NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCloseRichWorkspaceWebView), object: nil) @@ -445,6 +473,11 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCreateFolder), object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterFavoriteFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterDeleteFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterMoveFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterCopyFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterCreateFolder), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterFavoriteFile), object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDownloadStartFile), object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDownloadedFile), object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDownloadCancelFile), object: nil) @@ -456,7 +489,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterUpdateShare), object: nil) - removeImageCache(metadatas: self.dataSource.getMetadatas()) + dataSource.removeImageCache() } func isApplicationUpdated() -> Bool { @@ -488,29 +521,153 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { let viewController = presentationController.presentedViewController - + if viewController is NCViewerRichWorkspaceWebView { closeRichWorkspaceWebView() } } + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + + if let frame = tabBarController?.tabBar.frame { + tabBarSelect?.hostingController?.view.frame = frame + } + } + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + + func transferChange(status: String, + account: String, + fileName: String, + serverUrl: String, + selector: String?, + ocId: String, + destination: String?, + error: NKError) { + Task { + if error != .success, + error.errorCode != global.errorResourceNotFound { + await showErrorBanner(controller: self.controller, + errorDescription: error.errorDescription, + errorCode: error.errorCode) + } + guard session.account == account else { + return + } + + await self.debouncer.call { + switch status { + // UPLOADED, UPLOADED LIVEPHOTO, DELETE + case self.global.networkingStatusUploaded, + self.global.networkingStatusDelete, + self.global.networkingStatusCopyMove: + if self.isSearchingMode { + self.networkSearch() + } else if self.serverUrl == serverUrl || destination == self.serverUrl { + await self.reloadDataSource() + } + // DOWNLOAD + case self.global.networkingStatusDownloaded: + if serverUrl == self.serverUrl || self.serverUrl.isEmpty { + await self.reloadDataSource() + } + case self.global.networkingStatusDownloadCancel: + if serverUrl == self.serverUrl { + await self.reloadDataSource() + } + // CREATE FOLDER + case self.global.networkingStatusCreateFolder: + if serverUrl == self.serverUrl, + selector != self.global.selectorUploadAutoUpload, + let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { + self.pushMetadata(metadata) + } + // RENAME + case self.global.networkingStatusRename: + if self.isSearchingMode { + self.networkSearch() + } else if self.serverUrl == serverUrl { + await self.reloadDataSource() + } + // FAVORITE + case self.global.networkingStatusFavorite: + if self.isSearchingMode { + self.networkSearch() + } else if self is NCFavorite { + await self.reloadDataSource() + } else if self.serverUrl == serverUrl { + await self.reloadDataSource() + } + default: + break + } + } + } + } + + func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) { + Task { + await self.debouncer.call { + if requestData { + if self.isSearchingMode { + self.networkSearch() + } else if ( self.serverUrl == serverUrl) || serverUrl == nil { + Task { + await self.getServerData() + } + } + } else { + if self.isSearchingMode { + guard status != self.global.metadataStatusWaitDelete, + status != self.global.metadataStatusWaitRename, + status != self.global.metadataStatusWaitMove, + status != self.global.metadataStatusWaitCopy, + status != self.global.metadataStatusWaitFavorite else { + return + } + self.networkSearch() + } else if ( self.serverUrl == serverUrl) || serverUrl == nil { + Task { + await self.reloadDataSource() + } + } + } + } + } + } + // MARK: - NotificationCenter @objc func applicationWillResignActive(_ notification: NSNotification) { self.mainNavigationController?.menuPlus?.resetPlusButtonAlpha() +// self.resetPlusButtonAlpha() +// self.refreshControl.endRefreshing() } - @objc func closeRichWorkspaceWebView() { - Task { - await self.reloadDataSource() + @objc func reloadAvatar(_ notification: NSNotification) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.showTipAccounts() } + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError, + error.errorCode != global.errorNotModified else { return } + /// Magentacloud branding changes hide user account button on left navigation bar + setNavigationLeftItems() } - // MARK: - Layout + @objc func changeTheming(_ notification: NSNotification) { + self.reloadDataSource() + } - func changeLayout(layoutForView: NCDBLayoutForView) { - let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) - let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 + @objc func changeLayout(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let layoutForView = userInfo["layoutForView"] as? NCDBLayoutForView, + account == session.account, + serverUrl == self.serverUrl + else { return } if serverUrl == homeServer || numFoldersLayoutsForView == 1 { Task { @@ -532,9 +689,29 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, await self.reloadDataSource() } })) +// if self.layoutForView?.layout == layoutForView.layout { +// self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) +// self.reloadDataSource() +// return +// } +// +// self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) +// layoutForView.layout = layoutForView.layout +// self.layoutType = layoutForView.layout +// +// collectionView.reloadData() - self.present(alertController, animated: true) + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break } + self.collectionView.collectionViewLayout.invalidateLayout() // (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() @@ -820,16 +997,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, collectionView?.reloadData() } } - - @objc func updateShare(_ notification: NSNotification) { - if isSearchingMode { - networkSearch() - } else { - self.dataSource.removeAll() - getServerData() - } - } - + @objc func triggerProgressTask(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, let progressNumber = userInfo["progress"] as? NSNumber, @@ -844,14 +1012,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) - - // HEADER -// if self.headerMenuTransferView, transfer.session.contains("upload") { -// self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) -// self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) -// } - DispatchQueue.main.async { +// DispatchQueue.main.async { if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { if NCNetworking.shared.transferInForegorund?.ocId == ocId { NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue @@ -861,7 +1023,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, + guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId), let cell = self.collectionView?.cellForItem(at: indexPath), let cell = cell as? NCCellProtocol else { return } if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { @@ -893,8 +1055,22 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } } } +// } + } + + @objc func updateShare(_ notification: NSNotification) { + if isSearchingMode { + networkSearch() + } else { +// self.dataSource.removeAll() + getServerData() } } + + @objc func updateIcons() { + collectionView.reloadData() +// reloadDataSource() + } // MARK: - Layout @@ -903,14 +1079,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } func getNavigationTitle() -> String { - let tblAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) - if let tblAccount, - !tblAccount.alias.isEmpty { - return tblAccount.alias + let tableAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) + if let tableAccount, + !tableAccount.alias.isEmpty { + return tableAccount.alias } return NCBrandOptions.shared.brand } + func accountSettingsDidDismiss(tableAccount: tableAccount?, controller: NCMainTabBarController?) { } + @MainActor func startGUIGetServerData() { self.dataSource.setGetServerData(false) @@ -943,6 +1121,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.navigationItem.titleView = nil self.navigationItem.title = self.titleCurrentFolder } + + func resetPlusButtonAlpha(animated: Bool = true) { } + + func isHiddenPlusButton(_ isHidden: Bool) { } // MARK: - Empty @@ -1005,6 +1187,12 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.dataSource.removeAll() self.collectionView.reloadData() } +// isSearchingMode = true +// self.providers?.removeAll() +// self.dataSource.removeAll() +// self.reloadDataSource() +// // TIP +// dismissTip() } func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { @@ -1019,17 +1207,12 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - self.networking.cancelUnifiedSearchFiles() - + NCNetworking.shared.cancelUnifiedSearchFiles() self.isSearchingMode = false self.literalSearch = "" self.providers?.removeAll() self.dataSource.removeAll() - Task { - await self.reloadDataSource() - } - // - mainNavigationController?.hiddenPlusButton(false) + self.reloadDataSource() } // MARK: - TAP EVENT @@ -1037,6 +1220,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, func tapMoreListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) } + + func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) + } func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) @@ -1064,6 +1251,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } } } + + func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } +// toggleMenu(metadata: metadata, image: image) + Task { + await cancelSession(metadata: metadata) + } + } + + func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } func tapRichWorkspace(_ sender: Any) { if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { @@ -1081,16 +1278,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { // (+) self.mainNavigationController?.menuPlus?.hiddenPlusButton(false) - + self.isSearchingMode = false self.networkSearchInProgress = false self.searchResultText = nil self.searchResultStore = nil - + Task { await searchOperationHandle.cancel() await reloadDataSource() - + // Restore Layout if let layoutForViewLayoutStore { let layoutForView = database.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) @@ -1098,12 +1295,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, await setLayout(layoutForView: layoutForView) } layoutForViewLayoutStore = nil - + // update Option menu await mainNavigationController?.updateMenuOption() } } + func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?) { + toggleMenu(metadata: metadata, image: image) + } + @MainActor func setSearchBarLoading(_ loading: Bool) { guard let textField = searchController?.searchBar.searchTextField else { @@ -1143,32 +1344,28 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } func tapButtonOrder(_ sender: Any) { - -// if let titleButtonHeader = NCKeychain().getTitleButtonHeader(account: session.account), !titleButtonHeader.isEmpty { -// layoutForView?.titleButtonHeader = titleButtonHeader -// } -// NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView!) - let sortMenu = NCSortMenu() sortMenu.toggleMenu(viewController: self, account: session.account, key: layoutKey, sortButton: sender as? UIButton, serverUrl: serverUrl) } - func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func tapButtonTransfer(_ sender: Any) { } - func longPressListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } - func tapShareListItem(with objectId: String, indexPath: IndexPath, sender: Any) { } + func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func longPressListItem(with objectId: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) { + } + func longPressListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } func longPressMoreListItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } func longPressGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } func longPressMoreGridItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func tapButtonTransfer(_ sender: Any) { } - - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } - @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) } @@ -1181,14 +1378,130 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, }) } + func openMenuItems(with objectId: String?, gestureRecognizer: UILongPressGestureRecognizer) { + if gestureRecognizer.state != .began { return } + + var listMenuItems: [UIMenuItem] = [] + let touchPoint = gestureRecognizer.location(in: collectionView) + + becomeFirstResponder() + + if !serverUrl.isEmpty { + listMenuItems.append(UIMenuItem(title: NSLocalizedString("_paste_file_", comment: ""), action: #selector(pasteFilesMenu))) + } + + if !listMenuItems.isEmpty { + UIMenuController.shared.menuItems = listMenuItems + UIMenuController.shared.showMenu(from: collectionView, rect: CGRect(x: touchPoint.x, y: touchPoint.y, width: 0, height: 0)) + } + } + + // MARK: - Transfer Delegate + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + + func tranferChange(status: String, metadata: tableMetadata, error: NKError) { } + + // MARK: - Menu Item + + override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + + if #selector(pasteFilesMenu) == action { + if !UIPasteboard.general.items.isEmpty, !(metadataFolder?.e2eEncrypted ?? false) { + return true + } + } else if #selector(copyMenuFile) == action { + return true + } else if #selector(moveMenuFile) == action { + return true + } + + return false + } + + @objc func pasteFilesMenu(_ sender: Any?) { + Task {@MainActor in + guard let tblAccount = await NCManageDatabase.shared.getTableAccountAsync(account: session.account) else { + return + } + let scene = SceneManager.shared.getWindow(controller: controller)?.windowScene + let token = showHudBanner( + scene: scene, + title: NSLocalizedString("_upload_in_progress_", comment: "")) + + for (index, items) in UIPasteboard.general.items.enumerated() { + for item in items { + let capabilities = await NKCapabilities.shared.getCapabilities(for: session.account) + let results = NKFilePropertyResolver().resolve(inUTI: item.key, capabilities: capabilities) + guard let data = UIPasteboard.general.data(forPasteboardType: item.key, + inItemSet: IndexSet([index]))?.first + else { + continue + } + let fileName = results.name + "_" + NCPreferences().incrementalNumber + "." + results.ext + let serverUrlFileName = utilityFileSystem.createServerUrl(serverUrl: serverUrl, fileName: fileName) + let ocIdUpload = UUID().uuidString + let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId( + ocIdUpload, + fileName: fileName, + userId: tblAccount.userId, + urlBase: tblAccount.urlBase + ) + do { + try data.write(to: URL(fileURLWithPath: fileNameLocalPath)) + } catch { + continue + } + + let resultsUpload = await NCNetworking.shared.uploadFile(account: session.account, + fileNameLocalPath: fileNameLocalPath, + serverUrlFileName: serverUrlFileName) { _ in + } progressHandler: { _, _, fractionCompleted in + Task {@MainActor in + LucidBanner.shared.update(progress: fractionCompleted, for: token) + } + } + + if resultsUpload.error == .success, + let etag = resultsUpload.etag, + let ocId = resultsUpload.ocId { + let toPath = self.utilityFileSystem.getDirectoryProviderStorageOcId( + ocId, + fileName: fileName, + userId: tblAccount.userId, + urlBase: tblAccount.urlBase) + self.utilityFileSystem.moveFile(atPath: fileNameLocalPath, toPath: toPath) + NCManageDatabase.shared.addLocalFile( + account: session.account, + etag: etag, + ocId: ocId, + fileName: fileName) + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: serverUrl, requestData: true, status: nil) + } + } + } else { + Task {@MainActor in + await showErrorBanner(scene: scene, + errorDescription: resultsUpload.error.errorDescription, + errorCode: resultsUpload.error.errorCode) + } + } + } + } + LucidBanner.shared.dismiss() + } + } + // MARK: - DataSource - @objc func reloadDataSource() async { + @objc func reloadDataSource() { // get auto upload folder autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: session.urlBase, userId: session.userId, account: session.account) - + // get layout for view layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) // set GroupField for Grid @@ -1197,16 +1510,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } else { groupByField = "name" } - if isSearchingMode { isDirectoryEncrypted = false } else { isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) - if isRecommendationActived { - Task.detached { - await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) - } - } } await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in @@ -1221,7 +1528,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } } - func getServerData(forced: Bool = false) async { } + func getServerData() { + } // MARK: - Push metadata @@ -1291,38 +1599,55 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, navigationController.pushViewController(viewController, animated: true) } - + func pushViewController(viewController: UIViewController) { if pushed { return } pushed = true navigationController?.pushViewController(viewController, animated: true) } - + // MARK: - Header size -// func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, -// heightHeaderRecommendations: CGFloat, -// heightHeaderSection: CGFloat) { -// var heightHeaderRichWorkspace: CGFloat = 0 -// var heightHeaderRecommendations: CGFloat = 0 -// var heightHeaderSection: CGFloat = 0 +// func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { +// var headerRichWorkspace: CGFloat = 0 // -// if showDescription, -// !isSearchingMode, -// let richWorkspaceText = self.richWorkspaceText, -// !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { -// heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 -// } +// func getHeaderHeight() -> CGFloat { +// var size: CGFloat = 0 // -// if isRecommendationActived, -// !isSearchingMode, -// NCKeychain().showRecommendedFiles, -// !self.database.getRecommendedFiles(account: self.session.account).isEmpty { -// heightHeaderRecommendations = self.heightHeaderRecommendations -// heightHeaderSection = self.heightHeaderSection +// if isHeaderMenuTransferViewEnabled() != nil { +// if !isSearchingMode { +// size += global.heightHeaderTransfer +// } +// } +// if headerMenuButtonsView { +// size += NCGlobal.shared.heightButtonsView +// } +// return size // } // +//// func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, +//// heightHeaderRecommendations: CGFloat, +//// heightHeaderSection: CGFloat) { +//// var heightHeaderRichWorkspace: CGFloat = 0 +//// var heightHeaderRecommendations: CGFloat = 0 +//// var heightHeaderSection: CGFloat = 0 +//// +//// if showDescription, +//// !isSearchingMode, +//// let richWorkspaceText = self.richWorkspaceText, +//// !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { +//// heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 +//// } +//// +//// if isRecommendationActived, +//// !isSearchingMode, +//// NCKeychain().showRecommendedFiles, +//// !self.database.getRecommendedFiles(account: self.session.account).isEmpty { +//// heightHeaderRecommendations = self.heightHeaderRecommendations +//// heightHeaderSection = self.heightHeaderSection +//// } +// // if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { // if section == 0 { // return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) @@ -1331,6 +1656,15 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // } // } else { // return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) +//// +//// if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { +//// if section == 0 { +//// return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) +//// } else { +//// return (0, 0, self.heightHeaderSection) +//// } +//// } else { +//// return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) // } // } @@ -1354,8 +1688,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } else if isEditMode || (isLandscape && isIphone) { return CGSize.zero } else { - let (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) = getHeaderHeight(section: section) - height = heightHeaderRichWorkspace + heightHeaderRecommendations + heightHeaderSection + let (heightHeaderCommands, heightHeaderRichWorkspace, heightHeaderSection) = getHeaderHeight(section: section) + height = heightHeaderCommands + heightHeaderRichWorkspace + heightHeaderSection } return CGSize(width: collectionView.frame.width, height: height) @@ -1364,26 +1698,22 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // MARK: - Footer size func sizeForFooterInSection(section: Int) -> CGSize { - guard let controller else { - return CGSize.zero - } let sections = dataSource.numberOfSections() - let bottomAreaInsets: CGFloat = controller.tabBar.safeAreaInsets.bottom == 0 ? 34 : 0 - let height = controller.tabBar.frame.height + bottomAreaInsets - - if isEditMode { - return CGSize(width: collectionView.frame.width, height: 90 + height) - } - - if isSearchingMode { - return CGSize(width: collectionView.frame.width, height: 50) - } + let metadataForSection = self.dataSource.getMetadataForSection(section) + let isPaginated = metadataForSection?.lastSearchResult?.isPaginated ?? false + let metadatasCount: Int = metadataForSection?.lastSearchResult?.entries.count ?? 0 + var size = CGSize(width: collectionView.frame.width, height: 0) if section == sections - 1 { - return CGSize(width: collectionView.frame.width, height: height) + size.height += 85 } else { - return CGSize(width: collectionView.frame.width, height: 0) + size.height += 1 + } + + if isSearchingMode && isPaginated && metadatasCount > 0 { + size.height += 30 } + return size } func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } From 5a545f9f18c70c04431037b3141fa918ccfc797c Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:58:19 +0530 Subject: [PATCH 07/40] NMC 1984 - Privacy policy customisation added --- .../Collection Common/NCCollectionViewCommon.swift | 13 +++++++++++++ .../Settings/PrivacySettingsViewController.swift | 11 +---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 2e9fbed9f5..ecc5bcd219 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -504,6 +504,19 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS newViewController.modalPresentationStyle = .fullScreen self.present(newViewController, animated: true, completion: nil) } + + func isApplicationUpdated() -> Bool{ + let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" + let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") + return currentVersion != appVersion + } + + func redirectToPrivacyViewController(){ + let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) + let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController + newViewController.modalPresentationStyle = .fullScreen + self.present(newViewController, animated: true, completion: nil) + } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index b7883ad800..685bcef50a 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -12,7 +12,7 @@ import AdSupport class PrivacySettingsViewController: XLFormViewController{ - @objc public var isShowSettingsButton: Bool = false + @objc public var isShowSettingsButton: Bool = false override func viewDidLoad() { super.viewDidLoad() @@ -34,13 +34,9 @@ class PrivacySettingsViewController: XLFormViewController{ tableView.reloadData() initializeForm() } - - //MARK: XLForm - func initializeForm() { - let form : XLFormDescriptor = XLFormDescriptor() as XLFormDescriptor form.rowNavigationOptions = XLFormRowNavigationOptions.stopDisableRow @@ -57,7 +53,6 @@ class PrivacySettingsViewController: XLFormViewController{ section.footerTitle = NSLocalizedString("_privacy_settings_help_text_", comment: "") form.addFormSection(section) - //custom cell section = XLFormSectionDescriptor.formSection(withTitle: "") section.footerTitle = NSLocalizedString("_required_data_collection_help_text_", comment: "") @@ -66,7 +61,6 @@ class PrivacySettingsViewController: XLFormViewController{ XLFormViewController.cellClassesForRowDescriptorTypes()["RequiredDataCollectionCustomCellType"] = RequiredDataCollectionSwitch.self - row = XLFormRowDescriptor(tag: "ButtonDestinationFolder", rowType: "RequiredDataCollectionCustomCellType", title: "") row.cellConfig["requiredDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue @@ -81,7 +75,6 @@ class PrivacySettingsViewController: XLFormViewController{ XLFormViewController.cellClassesForRowDescriptorTypes()["AnalysisDataCollectionCustomCellType"] = AnalysisDataCollectionSwitch.self - row = XLFormRowDescriptor(tag: "AnalysisDataCollectionSwitch", rowType: "AnalysisDataCollectionCustomCellType", title: "") row.cellConfig["analysisDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue @@ -96,13 +89,11 @@ class PrivacySettingsViewController: XLFormViewController{ section.addFormRow(row) - XLFormViewController.cellClassesForRowDescriptorTypes()["SaveSettingsButton"] = SaveSettingsCustomButtonCell.self section = XLFormSectionDescriptor.formSection(withTitle: "") form.addFormSection(section) - row = XLFormRowDescriptor(tag: "SaveSettingsButton", rowType: "SaveSettingsButton", title: "") row.cellConfig["backgroundColor"] = UIColor.clear From 6c07e5f6fd0d1ff7c2e4a163f5304ad71df7444c Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Tue, 26 Dec 2023 13:29:37 +0530 Subject: [PATCH 08/40] NMC 1984 - Privacy setting view controller added --- iOSClient/Settings/PrivacySettingsViewController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index 685bcef50a..5e56a6bd8d 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -34,9 +34,11 @@ class PrivacySettingsViewController: XLFormViewController{ tableView.reloadData() initializeForm() } - + //MARK: XLForm + func initializeForm() { + let form : XLFormDescriptor = XLFormDescriptor() as XLFormDescriptor form.rowNavigationOptions = XLFormRowNavigationOptions.stopDisableRow From c23cdd7f13e8d7a442a362706bc49e330d0b0f63 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Wed, 9 Apr 2025 14:10:39 +0530 Subject: [PATCH 09/40] NMC 1984 - Privacy policy customisation changes added --- .../NCCollectionViewCommon.swift | 468 +++++++++++++++--- 1 file changed, 406 insertions(+), 62 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index ecc5bcd219..d8ab0b3b9e 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -505,13 +505,13 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.present(newViewController, animated: true, completion: nil) } - func isApplicationUpdated() -> Bool{ + func isApplicationUpdated() -> Bool { let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") return currentVersion != appVersion } - func redirectToPrivacyViewController(){ + func redirectToPrivacyViewController() { let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController newViewController.modalPresentationStyle = .fullScreen @@ -1085,6 +1085,399 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // reloadDataSource() } + // MARK: - Layout + + func changeLayout(layoutForView: NCDBLayoutForView) { + let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) + let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 + + func changeLayout(withSubFolders: Bool) { + if self.layoutForView?.layout == layoutForView.layout { + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + Task { + await self.reloadDataSource() + } + return + } + + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + layoutForView.layout = layoutForView.layout + self.layoutType = layoutForView.layout + + collectionView.reloadData() + + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break + } + + self.collectionView.collectionViewLayout.invalidateLayout() + + Task { + await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + } + } + + if serverUrl == homeServer || numFoldersLayoutsForView == 1 { + changeLayout(withSubFolders: false) + } else { + let alertController = UIAlertController(title: NSLocalizedString("_propagate_layout_", comment: ""), message: nil, preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default, handler: { _ in + changeLayout(withSubFolders: true) + })) + alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in + changeLayout(withSubFolders: false) + })) + + self.present(alertController, animated: true) + } + self.collectionView.collectionViewLayout.invalidateLayout() + +// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() + } + + @objc func reloadDataSource(_ notification: NSNotification) { + if let userInfo = notification.userInfo as? NSDictionary { + if let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } + } + + if let clearDataSource = userInfo["clearDataSource"] as? Bool, clearDataSource { + self.dataSource.removeAll() + } + } + + reloadDataSource() + } + + @objc func getServerData(_ notification: NSNotification) { + if let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } + } + + getServerData() + } + + @objc func reloadHeader(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + account == session.account + else { return } + + self.collectionView.reloadData() + } + + @objc func changeStatusFolderE2EE(_ notification: NSNotification) { + reloadDataSource() + } + + @objc func closeRichWorkspaceWebView() { + reloadDataSource() + } + + @objc func deleteFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError else { return } + + if error == .success { + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + } else { + NCContentPresenter().showError(error: error) + } + + reloadDataSource() + } + + @objc func copyMoveFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + account == session.account else { return } + + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + + if serverUrl == self.serverUrl { + reloadDataSource() + } + } + + @objc func renameFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let error = userInfo["error"] as? NKError, + account == session.account + else { return } + + if error == .success { + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + } + + if serverUrl == self.serverUrl { + if error != .success { + NCContentPresenter().showError(error: error) + } + reloadDataSource() + } else { + collectionView.reloadData() + } + } + + @objc func createFolder(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let account = userInfo["account"] as? String, + account == session.account, + let withPush = userInfo["withPush"] as? Bool, + let metadata = database.getMetadataFromOcId(ocId) + else { return } + + if isSearchingMode { + return networkSearch() + } + + if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { + reloadDataSource() + } else if withPush, metadata.serverUrl == self.serverUrl { + reloadDataSource() + if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { + if sceneIdentifier == controller?.sceneIdentifier { + pushMetadata(metadata) + } + } else { + pushMetadata(metadata) + } + } + } + + @objc func favoriteFile(_ notification: NSNotification) { + if isSearchingMode { + return networkSearch() + } + + if self is NCFavorite { + return reloadDataSource() + } + + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + serverUrl == self.serverUrl + else { return } + + reloadDataSource() + } + + @objc func downloadStartFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func downloadedFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func downloadCancelFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadStartFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + !isSearchingMode, + let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) + else { return } + + // Header view trasfer + if metadata.isTransferInForeground { + NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) + DispatchQueue.main.async { self.collectionView?.reloadData() } + } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadedFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadedLivePhoto(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadCancelFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func updateShare(_ notification: NSNotification) { + if isSearchingMode { + networkSearch() + } else { + self.dataSource.removeAll() + getServerData() + } + } + + @objc func triggerProgressTask(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let progressNumber = userInfo["progress"] as? NSNumber, + let totalBytes = userInfo["totalBytes"] as? Int64, + let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, + let ocId = userInfo["ocId"] as? String, + let ocIdTransfer = userInfo["ocIdTransfer"] as? String, + let session = userInfo["session"] as? String + else { return } + + let chunk: Int = userInfo["chunk"] as? Int ?? 0 + let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false + + let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) + + // HEADER +// if self.headerMenuTransferView, transfer.session.contains("upload") { +// self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) +// self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) +// } + + DispatchQueue.main.async { + if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { + if NCNetworking.shared.transferInForegorund?.ocId == ocId { + NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue + } else { + NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) + self.collectionView.reloadData() + } + self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue + } else { + guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, + let cell = self.collectionView?.cellForItem(at: indexPath), + let cell = cell as? NCCellProtocol else { return } + if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { + cell.fileProgressView?.isHidden = true + cell.fileProgressView?.progress = .zero + cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) + if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { + cell.writeInfoDateSize(date: metadata.date, size: metadata.size) + } else { + cell.fileInfoLabel?.text = "" + cell.fileSubinfoLabel?.text = "" + } + } else { + cell.fileProgressView?.isHidden = false + cell.fileProgressView?.progress = progressNumber.floatValue + cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) + let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal + if status == NCGlobal.shared.metadataStatusDownloading { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) + } else if status == NCGlobal.shared.metadataStatusUploading { + if totalBytes > 0 { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) + } else { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" + } + } + } + } + } + } + // MARK: - Layout func setNavigationLeftItems() { @@ -1379,6 +1772,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS func longPressMoreGridItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func tapButtonTransfer(_ sender: Any) { } + + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } + @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) } @@ -1514,7 +1911,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // get auto upload folder autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: session.urlBase, userId: session.userId, account: session.account) - // get layout for view layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) // set GroupField for Grid @@ -1620,66 +2016,14 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS navigationController?.pushViewController(viewController, animated: true) } - // MARK: - Header size + func pushViewController(viewController: UIViewController) { + if pushed { return } -// func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { -// var headerRichWorkspace: CGFloat = 0 -// -// func getHeaderHeight() -> CGFloat { -// var size: CGFloat = 0 -// -// if isHeaderMenuTransferViewEnabled() != nil { -// if !isSearchingMode { -// size += global.heightHeaderTransfer -// } -// } -// if headerMenuButtonsView { -// size += NCGlobal.shared.heightButtonsView -// } -// return size -// } -// -//// func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, -//// heightHeaderRecommendations: CGFloat, -//// heightHeaderSection: CGFloat) { -//// var heightHeaderRichWorkspace: CGFloat = 0 -//// var heightHeaderRecommendations: CGFloat = 0 -//// var heightHeaderSection: CGFloat = 0 -//// -//// if showDescription, -//// !isSearchingMode, -//// let richWorkspaceText = self.richWorkspaceText, -//// !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { -//// heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 -//// } -//// -//// if isRecommendationActived, -//// !isSearchingMode, -//// NCKeychain().showRecommendedFiles, -//// !self.database.getRecommendedFiles(account: self.session.account).isEmpty { -//// heightHeaderRecommendations = self.heightHeaderRecommendations -//// heightHeaderSection = self.heightHeaderSection -//// } -// -// if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { -// if section == 0 { -// return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) -// } else { -// return (0, 0, self.heightHeaderSection) -// } -// } else { -// return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) -//// -//// if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { -//// if section == 0 { -//// return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) -//// } else { -//// return (0, 0, self.heightHeaderSection) -//// } -//// } else { -//// return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) -// } -// } + pushed = true + navigationController?.pushViewController(viewController, animated: true) + } + + // MARK: - Header size func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { if headerMenuTransferView, From 5c93257d95d31ec5896353331c31de9a0dad3486 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Tue, 30 Sep 2025 22:00:01 +0530 Subject: [PATCH 10/40] NMC 1984 - Privacy policy customisation added --- .../NCCollectionViewCommon.swift | 123 +++++++----------- 1 file changed, 46 insertions(+), 77 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index d8ab0b3b9e..d30bc10a00 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -498,19 +498,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return currentVersion != appVersion } - func redirectToPrivacyViewController() { - let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) - let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController - newViewController.modalPresentationStyle = .fullScreen - self.present(newViewController, animated: true, completion: nil) - } - - func isApplicationUpdated() -> Bool { - let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" - let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") - return currentVersion != appVersion - } - func redirectToPrivacyViewController() { let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController @@ -834,7 +821,17 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS if serverUrl == self.serverUrl { reloadDataSource() + } + + @objc func reloadAvatar(_ notification: NSNotification) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.showTipAccounts() } + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError, + error.errorCode != global.errorNotModified else { return } + /// Magentacloud branding changes hide user account button on left navigation bar + setNavigationLeftItems() } @objc func renameFile(_ notification: NSNotification) { @@ -1091,53 +1088,30 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 - func changeLayout(withSubFolders: Bool) { - if self.layoutForView?.layout == layoutForView.layout { - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) - Task { - await self.reloadDataSource() - } - return - } - - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) - layoutForView.layout = layoutForView.layout - self.layoutType = layoutForView.layout - - collectionView.reloadData() - - switch layoutForView.layout { - case global.layoutList: - self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) - case global.layoutGrid: - self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) - case global.layoutPhotoSquare, global.layoutPhotoRatio: - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) - default: - break - } - - self.collectionView.collectionViewLayout.invalidateLayout() - Task { - await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() - } + if self.layoutForView?.layout == layoutForView.layout { + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) + self.reloadDataSource() + return } - if serverUrl == homeServer || numFoldersLayoutsForView == 1 { - changeLayout(withSubFolders: false) - } else { - let alertController = UIAlertController(title: NSLocalizedString("_propagate_layout_", comment: ""), message: nil, preferredStyle: .alert) + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) + layoutForView.layout = layoutForView.layout + self.layoutType = layoutForView.layout - alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default, handler: { _ in - changeLayout(withSubFolders: true) - })) - alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in - changeLayout(withSubFolders: false) - })) + collectionView.reloadData() - self.present(alertController, animated: true) + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break } + self.collectionView.collectionViewLayout.invalidateLayout() // (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() @@ -1402,16 +1376,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS collectionView?.reloadData() } } - - @objc func updateShare(_ notification: NSNotification) { - if isSearchingMode { - networkSearch() - } else { - self.dataSource.removeAll() - getServerData() - } - } - + @objc func triggerProgressTask(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, let progressNumber = userInfo["progress"] as? NSNumber, @@ -1426,14 +1391,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) - - // HEADER -// if self.headerMenuTransferView, transfer.session.contains("upload") { -// self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) -// self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) -// } - DispatchQueue.main.async { +// DispatchQueue.main.async { if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { if NCNetworking.shared.transferInForegorund?.ocId == ocId { NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue @@ -1443,7 +1402,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, + guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId), let cell = self.collectionView?.cellForItem(at: indexPath), let cell = cell as? NCCellProtocol else { return } if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { @@ -1475,8 +1434,22 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } } +// } + } + + @objc func updateShare(_ notification: NSNotification) { + if isSearchingMode { + networkSearch() + } else { +// self.dataSource.removeAll() + getServerData() } } + + @objc func updateIcons() { + collectionView.reloadData() +// reloadDataSource() + } // MARK: - Layout @@ -1772,10 +1745,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS func longPressMoreGridItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func tapButtonTransfer(_ sender: Any) { } - - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } - @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) } @@ -2022,7 +1991,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS pushed = true navigationController?.pushViewController(viewController, animated: true) } - + // MARK: - Header size func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { From 8a406064024d417d88f66ab2aa1b06d456a07e40 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Wed, 17 Dec 2025 18:29:46 +0530 Subject: [PATCH 11/40] NMC 1984 - Privacy policy customisation added --- .../FileProviderData.swift | 2 +- .../Data/NCManageDatabase+LocalFile.swift | 40 +- .../Data/NCManageDatabase+Metadata.swift | 66 + ...mon+CollectionViewDelegateFlowLayout.swift | 43 - .../NCCollectionViewCommon+SyncMetadata.swift | 14 +- .../NCCollectionViewCommon.swift | 1923 +++++------------ .../NCBackgroundLocationUploadManager.swift | 19 +- iOSClient/NCGlobal.swift | 56 +- .../E2EE/NCNetworkingE2EEUpload.swift | 2 +- iOSClient/Networking/NCConfigServer.swift | 25 +- .../Networking/NCNetworking+Download.swift | 62 +- .../Networking/NCNetworking+LivePhoto.swift | 25 +- .../NCNetworking+Synchronization.swift | 93 + iOSClient/Networking/NCNetworking+Task.swift | 25 +- .../Networking/NCNetworking+Upload.swift | 21 +- .../Networking/NCNetworking+WebDAV.swift | 69 +- iOSClient/Networking/NCNetworking.swift | 198 ++ .../Networking/NCNetworkingProcess.swift | 1 - iOSClient/Networking/NCService.swift | 25 +- .../AnalysisDataCollectionSwitch.swift | 1 + ...InitialPrivacySettingsViewController.swift | 1 + .../PrivacySettingsViewController.swift | 1 + .../RequiredDataCollectionSwitch.swift | 2 +- .../SaveSettingsCustomButtonCell.swift | 1 + .../en.lproj/Localizable.strings | 778 ++++++- 25 files changed, 1974 insertions(+), 1519 deletions(-) create mode 100644 iOSClient/Networking/NCNetworking+Synchronization.swift diff --git a/File Provider Extension/FileProviderData.swift b/File Provider Extension/FileProviderData.swift index 172beb53fe..d73e8f6669 100644 --- a/File Provider Extension/FileProviderData.swift +++ b/File Provider Extension/FileProviderData.swift @@ -32,7 +32,7 @@ class FileProviderData: NSObject { case workingSet } - // MARK: - + // MARK: - @discardableResult func setupAccount(domain: NSFileProviderDomain? = nil, diff --git a/iOSClient/Data/NCManageDatabase+LocalFile.swift b/iOSClient/Data/NCManageDatabase+LocalFile.swift index 2146dec1ad..334e5fba25 100644 --- a/iOSClient/Data/NCManageDatabase+LocalFile.swift +++ b/iOSClient/Data/NCManageDatabase+LocalFile.swift @@ -29,14 +29,17 @@ extension NCManageDatabase { // MARK: - Realm Write - /// Adds or updates multiple local file entries corresponding to the given metadata array. - /// Uses async Realm read + single write transaction. Assumes `tableLocalFile` has a primary key. /// - Parameters: - /// - metadatas: Array of `tableMetadata` to map into `tableLocalFile`. - /// - offline: Optional override for the `offline` flag applied to all items. - func addLocalFilesAsync(metadatas: [tableMetadata], offline: Bool? = nil) async { - guard !metadatas.isEmpty else { - return + /// - metadata: The `tableMetadata` containing file details. + /// - offline: Optional flag to mark the file as available offline. + /// - Returns: Nothing. Realm write is performed asynchronously. + func addLocalFileAsync(metadata: tableMetadata, offline: Bool? = nil) async { + // Read (non-blocking): safely detach from Realm thread + let existing: tableLocalFile? = performRealmRead { realm in + realm.objects(tableLocalFile.self) + .filter(NSPredicate(format: "ocId == %@", metadata.ocId)) + .first + .map { tableLocalFile(value: $0) } } // Extract ocIds for efficient lookup @@ -59,20 +62,19 @@ extension NCManageDatabase { // Reuse existing object or create a new one let local = existingMap[metadata.ocId] ?? tableLocalFile() - local.account = metadata.account - local.etag = metadata.etag - local.exifDate = NSDate() - local.exifLatitude = "-1" - local.exifLongitude = "-1" - local.ocId = metadata.ocId - local.fileName = metadata.fileName + addObject.account = metadata.account + addObject.etag = metadata.etag + addObject.exifDate = NSDate() + addObject.exifLatitude = "-1" + addObject.exifLongitude = "-1" + addObject.ocId = metadata.ocId + addObject.fileName = metadata.fileName - if let offline { - local.offline = offline - } - - realm.add(local, update: .all) + if let offline { + addObject.offline = offline } + + realm.add(addObject, update: .all) } } diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index 011ddbf681..9f06da7d92 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -151,10 +151,37 @@ extension tableMetadata { (fileNameView as NSString).deletingPathExtension } + var isRenameable: Bool { + if !NCMetadataPermissions.canRename(self) { + return false + } + if lock { + return false + } + if !isDirectoryE2EE && e2eEncrypted { + return false + } + return true + } + + var isPrintable: Bool { + if isDocumentViewableOnly { + return false + } + if ["application/pdf", "com.adobe.pdf"].contains(contentType) || contentType.hasPrefix("text/") || classFile == NKTypeClassFile.image.rawValue { + return true + } + return false + } + var isSavebleInCameraRoll: Bool { return (classFile == NKTypeClassFile.image.rawValue && contentType != "image/svg+xml") || classFile == NKTypeClassFile.video.rawValue } + var isDocumentViewableOnly: Bool { + sharePermissionsCollaborationServices == NCPermissions().permissionReadShare && classFile == NKTypeClassFile.document.rawValue + } + var isAudioOrVideo: Bool { return classFile == NKTypeClassFile.audio.rawValue || classFile == NKTypeClassFile.video.rawValue } @@ -344,6 +371,17 @@ extension tableMetadata { return !lock || (lockOwner == user && lockOwnerType == 0) } + // Return if is sharable + func isSharable() -> Bool { + guard let capabilities = NCNetworking.shared.capabilities[account] else { + return false + } + if !capabilities.fileSharingApiEnabled || (capabilities.e2EEEnabled && isDirectoryE2EE), !e2eEncrypted { + return false + } + return !e2eEncrypted + } + /// Returns a detached (unmanaged) deep copy of the current `tableMetadata` object. /// /// - Note: Primitive properties and lists of primitive values (for example `shareType`) @@ -918,6 +956,34 @@ extension NCManageDatabase { .map { $0.detachedCopy() } } ?? [] } + + func getMediaMetadatas(predicate: NSPredicate, sorted: String? = nil, ascending: Bool = false) -> ThreadSafeArray? { + + do { + let realm = try Realm() + if let sorted { + var results: [tableMetadata] = [] + switch sorted {//NCPreferences().mediaSortDate { + case "date": + results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.date as Date) > ($1.date as Date) } + case "creationDate": + results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.creationDate as Date) > ($1.creationDate as Date) } + case "uploadDate": + results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.uploadDate as Date) > ($1.uploadDate as Date) } + default: + let results = realm.objects(tableMetadata.self).filter(predicate) + return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) + } + return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) + } else { + let results = realm.objects(tableMetadata.self).filter(predicate) + return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) + } + } catch let error as NSError { +// NextcloudKit.shared.nkCommonInstance.writeLog("Could not access database: \(error)") + } + return nil + } func getMetadatas(predicate: NSPredicate, sortedByKeyPath: String, diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift index 42bbbd6038..06795639bc 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift @@ -13,47 +13,4 @@ extension NCCollectionViewCommon: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { return sizeForFooterInSection(section: section) } - - func getHeaderHeight() -> CGFloat { - - var size: CGFloat = 0 - // transfer in progress - if headerMenuTransferView, - let metadata = NCManageDatabase.shared.getMetadataFromOcId(NCNetworking.shared.transferInForegorund?.ocId), - metadata.isTransferInForeground { - if !isSearchingMode { - size += NCGlobal.shared.heightHeaderTransfer - } - } else { - NCNetworking.shared.transferInForegorund = nil - } - - if headerMenuButtonsView { - size += NCGlobal.shared.heightButtonsView - } - - return size - } - - func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { - - var headerRichWorkspace: CGFloat = 0 - - if let richWorkspaceText = richWorkspaceText, showDescription { - let trimmed = richWorkspaceText.trimmingCharacters(in: .whitespaces) - if !trimmed.isEmpty && !isSearchingMode { - headerRichWorkspace = UIScreen.main.bounds.size.height / 6 - } - } - - if isSearchingMode || layoutForView?.layout == NCGlobal.shared.layoutGrid || dataSource.numberOfSections() > 1 { - if section == 0 { - return (getHeaderHeight(), headerRichWorkspace, NCGlobal.shared.heightSection) - } else { - return (0, 0, NCGlobal.shared.heightSection) - } - } else { - return (getHeaderHeight(), headerRichWorkspace, 0) - } - } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift index 5e0254858d..571306d2de 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift @@ -21,7 +21,7 @@ extension NCCollectionViewCommon { // If a sync task is already running, do not start a new one if let task = syncMetadatasTask, !task.isCancelled { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .info, message: "Exit: Another sync is already running. Skipping this one.", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .info, message: "Exit: Another sync is already running. Skipping this one.", consoleOnly: true) return } @@ -45,9 +45,9 @@ extension NCCollectionViewCommon { func stopSyncMetadata() async { if let task = syncMetadatasTask { if task.isCancelled { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .stop, message: "Sync Metadata for \(self.serverUrl) was already cancelled.", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .stop, message: "Sync Metadata for \(self.serverUrl) was already cancelled.", consoleOnly: true) } else { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .stop, message: "Stopping active Sync Metadata for \(self.serverUrl).", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .stop, message: "Stopping active Sync Metadata for \(self.serverUrl).", consoleOnly: true) } } @@ -74,7 +74,7 @@ extension NCCollectionViewCommon { return } let identifier = self.serverUrl + "_syncMetadata" - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .start, message: "Start Sync Metadata for \(self.serverUrl)") + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .start, message: "Start Sync Metadata for \(self.serverUrl)") // Always cancel and clear all tracked URLSessionTask on any exit path defer { @@ -85,7 +85,7 @@ extension NCCollectionViewCommon { // If a readFile for this serverUrl is already in-flight, do nothing if await networking.networkingTasks.isReading(identifier: identifier) { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .debug, message: "ReadFile for this \(self.serverUrl) is already in-flight.", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .debug, message: "ReadFile for this \(self.serverUrl) is already in-flight.", consoleOnly: true) return } @@ -151,9 +151,9 @@ extension NCCollectionViewCommon { // If this folder failed, skip it but keep processing others if resultsReadFolder.error == .success { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .network, message: "Read correctly: \(serverUrl)", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .network, message: "Read correctly: \(serverUrl)", consoleOnly: true) } else { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .error, message: "Read failed for \(serverUrl) with error: \(resultsReadFolder.error.errorDescription)") + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .error, message: "Read failed for \(serverUrl) with error: \(resultsReadFolder.error.errorDescription)") return } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index d30bc10a00..2595fb1348 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -11,7 +11,7 @@ import EasyTipView import LucidBanner import MoEngageInApps -class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionHeaderMenuDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate, NCEmptyDataSetDelegate { +class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionFirstHeaderDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { @IBOutlet weak var collectionView: UICollectionView! @@ -21,6 +21,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let utilityFileSystem = NCUtilityFileSystem() let imageCache = NCImageCache.shared var dataSource = NCCollectionViewDataSource() + let networking = NCNetworking.shared let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() @@ -31,7 +32,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var backgroundImageView = UIImageView() var serverUrl: String = "" var isEditMode = false - var isDirectoryEncrypted = false + var isDirectoryE2EE = false var fileSelect: [String] = [] var metadataFolder: tableMetadata? var richWorkspaceText: String? @@ -40,7 +41,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var isSearchingMode: Bool = false var networkSearchInProgress: Bool = false var layoutForView: NCDBLayoutForView? - var dataSourceTask: URLSessionTask? + var searchDataSourceTask: URLSessionTask? var providers: [NKSearchProvider]? var searchResults: [NKSearchResult]? var listLayout = NCListLayout() @@ -56,123 +57,83 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // var headerMenu: NCSectionFirstHeader? var tipViewAccounts: EasyTipView? - var tipViewAutoUpload: EasyTipView? - var headerMenu: NCSectionHeaderMenu? - var headerMenuTransferView = false - var headerMenuButtonsView: Bool = true - var headerRichWorkspaceDisable: Bool = false - - // DECLARE - // - internal var layoutKey = "" - internal var titleCurrentFolder = "" - internal var titlePreviusFolder: String? - internal var enableSearchBar: Bool = false - internal var headerRichWorkspaceDisable: Bool = false - - internal var emptyImageName: String? - internal var emptyImageColors: [UIColor]? - internal var emptyTitle: String = "" + var selectableDataSource: [RealmSwiftObject] { dataSource.getMetadataSourceForAllSections() } - let maxImageGrid: CGFloat = 7 - var headerMenu: NCSectionHeaderMenu? - var headerMenuTransferView = false - var headerMenuButtonsView: Bool = true - var headerRichWorkspaceDisable: Bool = false - + // DECLARE + var layoutKey = "" + var titleCurrentFolder = "" + var titlePreviusFolder: String? + var enableSearchBar: Bool = false var groupByField = "name" + var headerRichWorkspaceDisable: Bool = false - var emptyImage: UIImage? + var emptyImageName: String? + var emptyImageColors: [UIColor]? + var emptyTitle: String = "" - internal var emptyDescription: String = "" - internal var emptyDataPortaitOffset: CGFloat = 0 - internal var emptyDataLandscapeOffset: CGFloat = -20 + var emptyDescription: String = "" + var emptyDataPortaitOffset: CGFloat = 0 + var emptyDataLandscapeOffset: CGFloat = -20 - internal var lastScale: CGFloat = 1.0 - internal var currentScale: CGFloat = 1.0 - internal var maxColumns: Int { + var lastScale: CGFloat = 1.0 + var currentScale: CGFloat = 1.0 + var maxColumns: Int { let screenWidth = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) let column = Int(screenWidth / 44) return column } - internal var transitionColumns = false - internal var numberOfColumns: Int = 0 - internal var lastNumberOfColumns: Int = 0 + var transitionColumns = false + var numberOfColumns: Int = 0 + var lastNumberOfColumns: Int = 0 - var isTransitioning: Bool = false - var selectableDataSource: [RealmSwiftObject] { dataSource.getMetadataSourceForAllSections() } - var pushed: Bool = false - var emptyDataSet: NCEmptyDataSet? - let heightHeaderRecommendations: CGFloat = 160 let heightHeaderSection: CGFloat = 30 + @MainActor var session: NCSession.Session { NCSession.shared.getSession(controller: tabBarController) } - internal var isLayoutPhoto: Bool { + var isLayoutPhoto: Bool { layoutForView?.layout == global.layoutPhotoRatio || layoutForView?.layout == global.layoutPhotoSquare } - internal var isLayoutGrid: Bool { + var isLayoutGrid: Bool { layoutForView?.layout == global.layoutGrid } - internal var isLayoutList: Bool { + var isLayoutList: Bool { layoutForView?.layout == global.layoutList } - internal var showDescription: Bool { + var showDescription: Bool { !headerRichWorkspaceDisable && NCPreferences().showDescription } - internal var isRecommendationActived: Bool { + var isRecommendationActived: Bool { let capabilities = NCNetworking.shared.capabilities[session.account] ?? NKCapabilities.Capabilities() return self.serverUrl == self.utilityFileSystem.getHomeServer(session: self.session) && capabilities.recommendations } - internal var infoLabelsSeparator: String { + var infoLabelsSeparator: String { layoutForView?.layout == global.layoutList ? " - " : "" } @MainActor - internal var session: NCSession.Session { - NCSession.shared.getSession(controller: tabBarController) - } - - @MainActor - internal var controller: NCMainTabBarController? { + var controller: NCMainTabBarController? { self.tabBarController as? NCMainTabBarController } - @MainActor - internal var mainNavigationController: NCMainNavigationController? { + var mainNavigationController: NCMainNavigationController? { self.navigationController as? NCMainNavigationController } - @MainActor - internal var sceneIdentifier: String { + var sceneIdentifier: String { (self.tabBarController as? NCMainTabBarController)?.sceneIdentifier ?? "" } - var defaultPredicate: NSPredicate { - let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, NCGlobal.shared.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) - return predicate - } - - var personalFilesOnlyPredicate: NSPredicate { - let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND (ownerId == %@ || ownerId == '') AND mountType == '' AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, session.userId, global.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) - return predicate - } - - @MainActor - internal var windowScene: UIWindowScene? { - SceneManager.shared.getWindowScene(controller: self.tabBarController as? NCMainTabBarController) - } - - internal var isNumberOfItemsInAllSectionsNull: Bool { + var isNumberOfItemsInAllSectionsNull: Bool { var totalItems = 0 for section in 0.. Bool { - let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" - let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") - return currentVersion != appVersion + + removeImageCache(metadatas: self.dataSource.getMetadatas()) } - func redirectToPrivacyViewController() { - let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) - let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController - newViewController.modalPresentationStyle = .fullScreen - self.present(newViewController, animated: true, completion: nil) + func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { + let viewController = presentationController.presentedViewController + + if viewController is NCViewerRichWorkspaceWebView { + closeRichWorkspaceWebView() + } } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) coordinator.animate(alongsideTransition: { _ in - self.collectionView?.collectionViewLayout.invalidateLayout() + let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) { + self.collectionView?.collectionViewLayout.invalidateLayout() + } + animator.startAnimation() }) self.dismissTip() @@ -518,20 +368,200 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS override var canBecomeFirstResponder: Bool { return true } + + @objc func updateIcons() { + collectionView.reloadData() + } - func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { - let viewController = presentationController.presentedViewController - - if viewController is NCViewerRichWorkspaceWebView { - closeRichWorkspaceWebView() + func isApplicationUpdated() -> Bool { + let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" + let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") + return currentVersion != appVersion + } + + func redirectToPrivacyViewController() { + let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) + let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as? UINavigationController + newViewController?.modalPresentationStyle = .fullScreen + self.present(newViewController!, animated: true, completion: nil) + } + + // MARK: - Transfer Delegate + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + + func transferChange(status: String, metadatasError: [tableMetadata: NKError]) { + switch status { + // DELETE + case self.global.networkingStatusDelete: + let errorForThisServer = metadatasError.first { entry in + let (key, value) = entry + return key.serverUrl == self.serverUrl && value != .success + }?.value + + let needLoadDataSource = metadatasError.contains { entry in + let (key, value) = entry + return key.serverUrl == self.serverUrl && value == .success + } + + if let error = errorForThisServer { + NCContentPresenter().showError(error: error) + } + + if self.isSearchingMode { + self.networkSearch() + } else if needLoadDataSource { + Task { + await self.reloadDataSource() + } + } else { + Task.detached { + if await self.isRecommendationActived() { + await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) + } + } + } + default: + break } } - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() + func transferChange(status: String, metadata: tableMetadata, error: NKError) { + guard session.account == metadata.account else { return } - if let frame = tabBarController?.tabBar.frame { - tabBarSelect?.hostingController?.view.frame = frame + if error != .success { + NCContentPresenter().showError(error: error) + } + + DispatchQueue.main.async { + switch status { + // UPLOADED, UPLOADED LIVEPHOTO + case self.global.networkingStatusUploaded, self.global.networkingStatusUploadedLivePhoto: + self.debouncer.call { + if self.isSearchingMode { + self.networkSearch() + } else if self.serverUrl == metadata.serverUrl { + Task { + await self.reloadDataSource() + } + } + } + // DOWNLOAD + case self.global.networkingStatusDownloading: + Task { + if metadata.serverUrl == self.serverUrl { + await self.reloadDataSource() + } + } + case self.global.networkingStatusDownloaded: + Task { + if metadata.serverUrl == self.serverUrl { + await self.reloadDataSource() + } + } + case self.global.networkingStatusDownloadCancel: + Task { + if metadata.serverUrl == self.serverUrl { + await self.reloadDataSource() + } + } + // CREATE FOLDER + case self.global.networkingStatusCreateFolder: + if metadata.serverUrl == self.serverUrl, metadata.sessionSelector != self.global.selectorUploadAutoUpload { + self.pushMetadata(metadata) + } + // RENAME + case self.global.networkingStatusRename: + self.debouncer.call { + if self.isSearchingMode { + self.networkSearch() + } else if self.serverUrl == metadata.serverUrl { + Task { + await self.reloadDataSource() + } + } + } + // FAVORITE + case self.global.networkingStatusFavorite: + self.debouncer.call { + if self.isSearchingMode { + self.networkSearch() + } else if self is NCFavorite { + Task { + await self.reloadDataSource() + } + } else if self.serverUrl == metadata.serverUrl { + Task { + await self.reloadDataSource() + } + } + } + default: + break + } + } + } + + func transferReloadData(serverUrl: String?, status: Int?) { + self.debouncer.call { + if self.isSearchingMode { + guard status != self.global.metadataStatusWaitDelete, + status != self.global.metadataStatusWaitRename, + status != self.global.metadataStatusWaitMove, + status != self.global.metadataStatusWaitCopy, + status != self.global.metadataStatusWaitFavorite else { + return + } + self.networkSearch() + } else if ( self.serverUrl == serverUrl) || serverUrl == nil { + Task { + await self.reloadDataSource() + } + } + } + } + + func transferRequestData(serverUrl: String?) { + self.debouncer.call { + if self.isSearchingMode { + self.networkSearch() + } else if ( self.serverUrl == serverUrl) || serverUrl == nil { + Task { + await self.getServerData() + } + } + } + } + + func transferCopy(metadata: tableMetadata, destination: String, error: NKError) { + if error != .success { + NCContentPresenter().showError(error: error) + } + + if isSearchingMode { + return networkSearch() + } + + if metadata.serverUrl == self.serverUrl || destination == self.serverUrl { + Task { + await self.reloadDataSource() + } + } + } + + func transferMove(metadata: tableMetadata, destination: String, error: NKError) { + if error != .success { + NCContentPresenter().showError(error: error) + } + + if isSearchingMode { + return networkSearch() + } + + if metadata.serverUrl == self.serverUrl || destination == self.serverUrl { + Task { + await self.reloadDataSource() + } } } @@ -640,837 +670,85 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // MARK: - NotificationCenter @objc func applicationWillResignActive(_ notification: NSNotification) { - self.mainNavigationController?.menuPlus?.resetPlusButtonAlpha() -// self.resetPlusButtonAlpha() -// self.refreshControl.endRefreshing() + mainNavigationController?.resetPlusButtonAlpha() } - @objc func reloadAvatar(_ notification: NSNotification) { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.showTipAccounts() + @objc func closeRichWorkspaceWebView() { + Task { + await self.reloadDataSource() } - guard let userInfo = notification.userInfo as NSDictionary?, - let error = userInfo["error"] as? NKError, - error.errorCode != global.errorNotModified else { return } - /// Magentacloud branding changes hide user account button on left navigation bar - setNavigationLeftItems() } - @objc func changeTheming(_ notification: NSNotification) { - self.reloadDataSource() - } + // MARK: - Layout + + func changeLayout(layoutForView: NCDBLayoutForView) { + let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) + let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 - @objc func changeLayout(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let account = userInfo["account"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let layoutForView = userInfo["layoutForView"] as? NCDBLayoutForView, - account == session.account, - serverUrl == self.serverUrl - else { return } + func changeLayout(withSubFolders: Bool) { + if self.layoutForView?.layout == layoutForView.layout { + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + Task { + await self.reloadDataSource() + } + return + } + + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + layoutForView.layout = layoutForView.layout + self.layoutType = layoutForView.layout + + collectionView.reloadData() + + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break + } + + self.collectionView.collectionViewLayout.invalidateLayout() - if serverUrl == homeServer || numFoldersLayoutsForView == 1 { Task { - await setLayout(layoutForView: layoutForView) - await self.reloadDataSource() + await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() } + } + + if serverUrl == homeServer || numFoldersLayoutsForView == 1 { + changeLayout(withSubFolders: false) } else { let alertController = UIAlertController(title: NSLocalizedString("_propagate_layout_", comment: ""), message: nil, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default, handler: { _ in - Task { - await self.setLayout(layoutForView: layoutForView, withSubFolders: true) - await self.reloadDataSource() - } + changeLayout(withSubFolders: true) })) alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in - Task { - await self.setLayout(layoutForView: layoutForView) - await self.reloadDataSource() - } + changeLayout(withSubFolders: false) })) -// if self.layoutForView?.layout == layoutForView.layout { -// self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) -// self.reloadDataSource() -// return -// } -// -// self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) -// layoutForView.layout = layoutForView.layout -// self.layoutType = layoutForView.layout -// -// collectionView.reloadData() - - switch layoutForView.layout { - case global.layoutList: - self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) - case global.layoutGrid: - self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) - case global.layoutPhotoSquare, global.layoutPhotoRatio: - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) - default: - break - } - self.collectionView.collectionViewLayout.invalidateLayout() - -// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() + self.present(alertController, animated: true) + } } - internal func setLayout(layoutForView: NCDBLayoutForView, withSubFolders: Bool = false) async { - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) - layoutForView.layout = layoutForView.layout - self.layoutType = layoutForView.layout - - collectionView.reloadData() - - switch layoutForView.layout { - case global.layoutList: - self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) - case global.layoutGrid: - self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) - case global.layoutPhotoSquare, global.layoutPhotoRatio: - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) - default: - break - } - - collectionView.collectionViewLayout.invalidateLayout() - } - - @objc func reloadDataSource(_ notification: NSNotification) { - if let userInfo = notification.userInfo as? NSDictionary { - if let serverUrl = userInfo["serverUrl"] as? String { - if serverUrl != self.serverUrl { - return - } - } - - if let clearDataSource = userInfo["clearDataSource"] as? Bool, clearDataSource { - self.dataSource.removeAll() - } - } - - reloadDataSource() - } - - @objc func getServerData(_ notification: NSNotification) { - if let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String { - if serverUrl != self.serverUrl { - return - } - } - - getServerData() - } - - @objc func reloadHeader(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let account = userInfo["account"] as? String, - account == session.account - else { return } - - self.collectionView.reloadData() - } - - @objc func changeStatusFolderE2EE(_ notification: NSNotification) { - reloadDataSource() - } - - @objc func closeRichWorkspaceWebView() { - reloadDataSource() - } - - @objc func deleteFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let error = userInfo["error"] as? NKError else { return } - - if error == .success { - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - } else { - NCContentPresenter().showError(error: error) - } - - reloadDataSource() - } - - @objc func copyMoveFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - account == session.account else { return } - - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - - if serverUrl == self.serverUrl { - reloadDataSource() - } - - @objc func reloadAvatar(_ notification: NSNotification) { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.showTipAccounts() - } - guard let userInfo = notification.userInfo as NSDictionary?, - let error = userInfo["error"] as? NKError, - error.errorCode != global.errorNotModified else { return } - /// Magentacloud branding changes hide user account button on left navigation bar - setNavigationLeftItems() - } - - @objc func renameFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let account = userInfo["account"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let error = userInfo["error"] as? NKError, - account == session.account - else { return } - - if error == .success { - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - } - - if serverUrl == self.serverUrl { - if error != .success { - NCContentPresenter().showError(error: error) - } - reloadDataSource() - } else { - collectionView.reloadData() - } - } - - @objc func createFolder(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let account = userInfo["account"] as? String, - account == session.account, - let withPush = userInfo["withPush"] as? Bool, - let metadata = database.getMetadataFromOcId(ocId) - else { return } - - if isSearchingMode { - return networkSearch() - } - - if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { - reloadDataSource() - } else if withPush, metadata.serverUrl == self.serverUrl { - reloadDataSource() - if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { - if sceneIdentifier == controller?.sceneIdentifier { - pushMetadata(metadata) - } - } else { - pushMetadata(metadata) - } - } - } - - @objc func favoriteFile(_ notification: NSNotification) { - if isSearchingMode { - return networkSearch() - } - - if self is NCFavorite { - return reloadDataSource() - } - - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - serverUrl == self.serverUrl - else { return } - - reloadDataSource() - } - - @objc func downloadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func downloadedFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func downloadCancelFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - !isSearchingMode, - let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) - else { return } - - // Header view trasfer - if metadata.isTransferInForeground { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) - DispatchQueue.main.async { self.collectionView?.reloadData() } - } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadedFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadedLivePhoto(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadCancelFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func triggerProgressTask(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let progressNumber = userInfo["progress"] as? NSNumber, - let totalBytes = userInfo["totalBytes"] as? Int64, - let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, - let ocId = userInfo["ocId"] as? String, - let ocIdTransfer = userInfo["ocIdTransfer"] as? String, - let session = userInfo["session"] as? String - else { return } - - let chunk: Int = userInfo["chunk"] as? Int ?? 0 - let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false - - let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) - -// DispatchQueue.main.async { - if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { - if NCNetworking.shared.transferInForegorund?.ocId == ocId { - NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue - } else { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) - self.collectionView.reloadData() - } - self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue - } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId), - let cell = self.collectionView?.cellForItem(at: indexPath), - let cell = cell as? NCCellProtocol else { return } - if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { - cell.fileProgressView?.isHidden = true - cell.fileProgressView?.progress = .zero - cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) - if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { - cell.writeInfoDateSize(date: metadata.date, size: metadata.size) - } else { - cell.fileInfoLabel?.text = "" - cell.fileSubinfoLabel?.text = "" - } - } else { - cell.fileProgressView?.isHidden = false - cell.fileProgressView?.progress = progressNumber.floatValue - cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) - let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal - if status == NCGlobal.shared.metadataStatusDownloading { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) - } else if status == NCGlobal.shared.metadataStatusUploading { - if totalBytes > 0 { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) - } else { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" - } - } - } - } -// } - } - - @objc func updateShare(_ notification: NSNotification) { - if isSearchingMode { - networkSearch() - } else { -// self.dataSource.removeAll() - getServerData() - } - } - - @objc func updateIcons() { - collectionView.reloadData() -// reloadDataSource() - } - - // MARK: - Layout - - func changeLayout(layoutForView: NCDBLayoutForView) { - let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) - let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 - - - if self.layoutForView?.layout == layoutForView.layout { - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) - self.reloadDataSource() - return - } - - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) - layoutForView.layout = layoutForView.layout - self.layoutType = layoutForView.layout - - collectionView.reloadData() - - switch layoutForView.layout { - case global.layoutList: - self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) - case global.layoutGrid: - self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) - case global.layoutPhotoSquare, global.layoutPhotoRatio: - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) - default: - break - } - - self.collectionView.collectionViewLayout.invalidateLayout() - -// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() - } - - @objc func reloadDataSource(_ notification: NSNotification) { - if let userInfo = notification.userInfo as? NSDictionary { - if let serverUrl = userInfo["serverUrl"] as? String { - if serverUrl != self.serverUrl { - return - } - } - - if let clearDataSource = userInfo["clearDataSource"] as? Bool, clearDataSource { - self.dataSource.removeAll() - } - } - - reloadDataSource() - } - - @objc func getServerData(_ notification: NSNotification) { - if let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String { - if serverUrl != self.serverUrl { - return - } - } - - getServerData() - } - - @objc func reloadHeader(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let account = userInfo["account"] as? String, - account == session.account - else { return } - - self.collectionView.reloadData() - } - - @objc func changeStatusFolderE2EE(_ notification: NSNotification) { - reloadDataSource() - } - - @objc func closeRichWorkspaceWebView() { - reloadDataSource() - } - - @objc func deleteFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let error = userInfo["error"] as? NKError else { return } - - if error == .success { - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - } else { - NCContentPresenter().showError(error: error) - } - - reloadDataSource() - } - - @objc func copyMoveFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - account == session.account else { return } - - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - - if serverUrl == self.serverUrl { - reloadDataSource() - } - } - - @objc func renameFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let account = userInfo["account"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let error = userInfo["error"] as? NKError, - account == session.account - else { return } - - if error == .success { - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - } - - if serverUrl == self.serverUrl { - if error != .success { - NCContentPresenter().showError(error: error) - } - reloadDataSource() - } else { - collectionView.reloadData() - } - } - - @objc func createFolder(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let account = userInfo["account"] as? String, - account == session.account, - let withPush = userInfo["withPush"] as? Bool, - let metadata = database.getMetadataFromOcId(ocId) - else { return } - - if isSearchingMode { - return networkSearch() - } - - if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { - reloadDataSource() - } else if withPush, metadata.serverUrl == self.serverUrl { - reloadDataSource() - if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { - if sceneIdentifier == controller?.sceneIdentifier { - pushMetadata(metadata) - } - } else { - pushMetadata(metadata) - } - } - } - - @objc func favoriteFile(_ notification: NSNotification) { - if isSearchingMode { - return networkSearch() - } - - if self is NCFavorite { - return reloadDataSource() - } - - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - serverUrl == self.serverUrl - else { return } - - reloadDataSource() - } - - @objc func downloadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func downloadedFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func downloadCancelFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - !isSearchingMode, - let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) - else { return } - - // Header view trasfer - if metadata.isTransferInForeground { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) - DispatchQueue.main.async { self.collectionView?.reloadData() } - } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadedFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadedLivePhoto(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadCancelFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func triggerProgressTask(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let progressNumber = userInfo["progress"] as? NSNumber, - let totalBytes = userInfo["totalBytes"] as? Int64, - let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, - let ocId = userInfo["ocId"] as? String, - let ocIdTransfer = userInfo["ocIdTransfer"] as? String, - let session = userInfo["session"] as? String - else { return } - - let chunk: Int = userInfo["chunk"] as? Int ?? 0 - let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false - - let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) - -// DispatchQueue.main.async { - if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { - if NCNetworking.shared.transferInForegorund?.ocId == ocId { - NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue - } else { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) - self.collectionView.reloadData() - } - self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue - } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId), - let cell = self.collectionView?.cellForItem(at: indexPath), - let cell = cell as? NCCellProtocol else { return } - if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { - cell.fileProgressView?.isHidden = true - cell.fileProgressView?.progress = .zero - cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) - if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { - cell.writeInfoDateSize(date: metadata.date, size: metadata.size) - } else { - cell.fileInfoLabel?.text = "" - cell.fileSubinfoLabel?.text = "" - } - } else { - cell.fileProgressView?.isHidden = false - cell.fileProgressView?.progress = progressNumber.floatValue - cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) - let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal - if status == NCGlobal.shared.metadataStatusDownloading { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) - } else if status == NCGlobal.shared.metadataStatusUploading { - if totalBytes > 0 { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) - } else { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" - } - } - } - } -// } - } - - @objc func updateShare(_ notification: NSNotification) { - if isSearchingMode { - networkSearch() - } else { -// self.dataSource.removeAll() - getServerData() - } - } - - @objc func updateIcons() { - collectionView.reloadData() -// reloadDataSource() - } - - // MARK: - Layout - - func setNavigationLeftItems() { - navigationItem.title = titleCurrentFolder - } - func getNavigationTitle() -> String { - let tableAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) - if let tableAccount, - !tableAccount.alias.isEmpty { - return tableAccount.alias + let tblAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) + if let tblAccount, + !tblAccount.alias.isEmpty { + return tblAccount.alias } return NCBrandOptions.shared.brand } - func accountSettingsDidDismiss(tableAccount: tableAccount?, controller: NCMainTabBarController?) { } + func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } @MainActor func startGUIGetServerData() { self.dataSource.setGetServerData(false) + self.collectionView.reloadData() // Don't show spinner on iPad root folder if UIDevice.current.userInterfaceIdiom == .pad, @@ -1533,83 +811,90 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS view.emptyDescription.text = NSLocalizedString("_no_file_pull_down_", comment: "") } } + + let spinner = UIActivityIndicatorView(style: .medium) + spinner.startAnimating() + + let container = UIView() + container.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(spinner) + + spinner.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + spinner.centerXAnchor.constraint(equalTo: container.centerXAnchor), + spinner.centerYAnchor.constraint(equalTo: container.centerYAnchor) + ]) + + self.navigationItem.titleView = container } - + + @MainActor + func restoreDefaultTitle() { + self.navigationItem.titleView = nil + self.navigationItem.title = self.titleCurrentFolder + } + // MARK: - SEARCH func searchController(enabled: Bool) { - guard enableSearchBar else { - return - } + guard enableSearchBar else { return } searchController?.searchBar.isUserInteractionEnabled = enabled - if enabled { searchController?.searchBar.alpha = 1 } else { searchController?.searchBar.alpha = 0.3 + } } func updateSearchResults(for searchController: UISearchController) { - searchResultText = searchController.searchBar.text + self.literalSearch = searchController.searchBar.text } func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + isSearchingMode = true + self.providers?.removeAll() + self.dataSource.removeAll() + Task { + await self.reloadDataSource() + } // TIP dismissTip() - - // (+) - self.mainNavigationController?.menuPlus?.hiddenPlusButton(true) - - if !isSearchingMode { - self.isSearchingMode = true - self.dataSource.removeAll() - self.collectionView.reloadData() - } -// isSearchingMode = true -// self.providers?.removeAll() -// self.dataSource.removeAll() -// self.reloadDataSource() -// // TIP -// dismissTip() + // + mainNavigationController?.hiddenPlusButton(true) } func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { - if isSearchingMode, - searchResultText?.count ?? 0 >= 2 { - Task { - await self.search() - } -// if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { -// networkSearch() + if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { + networkSearch() } } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - NCNetworking.shared.cancelUnifiedSearchFiles() + self.networking.cancelUnifiedSearchFiles() + self.isSearchingMode = false self.literalSearch = "" self.providers?.removeAll() self.dataSource.removeAll() - self.reloadDataSource() + Task { + await self.reloadDataSource() + } + // + mainNavigationController?.hiddenPlusButton(false) } // MARK: - TAP EVENT - func tapMoreListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { - tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) - } - func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) } - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { - tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) } func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) { - if isEditMode { return } guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) @@ -1618,29 +903,11 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS appDelegate.adjust.trackEvent(TriggerEvent(Sharing.rawValue)) } - func tapMoreGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { - if isEditMode { return } - guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } -// toggleMenu(metadata: metadata, image: image) - if namedButtonMore == NCGlobal.shared.buttonMoreMore || namedButtonMore == NCGlobal.shared.buttonMoreLock { - toggleMenu(metadata: metadata, image: image) - } else if namedButtonMore == NCGlobal.shared.buttonMoreStop { - Task { - await cancelSession(metadata: metadata) - } - } - } - func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } -// toggleMenu(metadata: metadata, image: image) - Task { - await cancelSession(metadata: metadata) - } + toggleMenu(metadata: metadata, image: image, sender: sender) } - func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func tapRichWorkspace(_ sender: Any) { if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { @@ -1654,97 +921,28 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } - func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - // (+) - self.mainNavigationController?.menuPlus?.hiddenPlusButton(false) - - self.isSearchingMode = false - self.networkSearchInProgress = false - self.searchResultText = nil - self.searchResultStore = nil - - Task { - await searchOperationHandle.cancel() - await reloadDataSource() - - // Restore Layout - if let layoutForViewLayoutStore { - let layoutForView = database.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) - layoutForView.layout = layoutForViewLayoutStore - await setLayout(layoutForView: layoutForView) - } - layoutForViewLayoutStore = nil - - // update Option menu - await mainNavigationController?.updateMenuOption() - } - } - - func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?) { - toggleMenu(metadata: metadata, image: image) + func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { + toggleMenu(metadata: metadata, image: image, sender: sender) } - @MainActor - func setSearchBarLoading(_ loading: Bool) { - guard let textField = searchController?.searchBar.searchTextField else { - return - } - if loading { - let spinner = UIActivityIndicatorView(style: .medium) - spinner.startAnimating() - textField.rightView = spinner - textField.rightViewMode = .always - } else { - textField.rightView = nil - } + func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { + unifiedSearchMore(metadataForSection: metadataForSection) } - // MARK: - TAP EVENT - func tapRecommendations(with metadata: tableMetadata) { didSelectMetadata(metadata, withOcIds: false) } - func tapButtonSwitch(_ sender: Any) { - guard !isTransitioning else { return } - isTransitioning = true - - guard let layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) else { return } - - if layoutForView.layout == NCGlobal.shared.layoutGrid { - layoutForView.layout = NCGlobal.shared.layoutList - } else { - layoutForView.layout = NCGlobal.shared.layoutGrid - } - self.layoutForView = NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) - self.collectionView.reloadData() - self.collectionView.collectionViewLayout.invalidateLayout() - self.collectionView.setCollectionViewLayout(layoutForView.layout == NCGlobal.shared.layoutList ? self.listLayout : self.gridLayout, animated: true) {_ in self.isTransitioning = false } - } - - func tapButtonOrder(_ sender: Any) { - let sortMenu = NCSortMenu() - sortMenu.toggleMenu(viewController: self, account: session.account, key: layoutKey, sortButton: sender as? UIButton, serverUrl: serverUrl) - } - - func tapButtonTransfer(_ sender: Any) { } - - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } - func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func longPressListItem(with objectId: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) { - } - func longPressListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func longPressGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func longPressMoreListItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - - func longPressGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } - - func longPressMoreGridItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } - + + func longPressMoreGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) } @@ -1766,7 +964,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS becomeFirstResponder() if !serverUrl.isEmpty { - listMenuItems.append(UIMenuItem(title: NSLocalizedString("_paste_file_", comment: ""), action: #selector(pasteFilesMenu))) + listMenuItems.append(UIMenuItem(title: NSLocalizedString("_paste_file_", comment: ""), action: #selector(pasteFilesMenu(_:)))) } if !listMenuItems.isEmpty { @@ -1775,23 +973,16 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } - // MARK: - Transfer Delegate - - func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } - - func tranferChange(status: String, metadata: tableMetadata, error: NKError) { } - // MARK: - Menu Item override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - - if #selector(pasteFilesMenu) == action { + if #selector(pasteFilesMenu(_:)) == action { if !UIPasteboard.general.items.isEmpty, !(metadataFolder?.e2eEncrypted ?? false) { return true } - } else if #selector(copyMenuFile) == action { + } else if #selector(copyMenuFile(_:)) == action { return true - } else if #selector(moveMenuFile) == action { + } else if #selector(moveMenuFile(_:)) == action { return true } @@ -1894,95 +1085,168 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) } - await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: self.serverUrl) + DispatchQueue.main.async { +// UIView.transition(with: self.collectionView, +// duration: 0.20, +// options: .transitionCrossDissolve, +// animations: { self.collectionView.reloadData() }, +// completion: nil) +// +// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() + if isRecommendationActived { + Task.detached { + await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) + } + } } - await mainNavigationController?.updateMenuOption() - (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + DispatchQueue.main.async { +// UIView.transition(with: self.collectionView, +// duration: 0.20, +// options: .transitionCrossDissolve, +// animations: { self.collectionView.reloadData() }, +// completion: nil) +// +// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() self.refreshControl.endRefreshing() self.collectionView.reloadData() self.setNavigationRightItems() } } - func getServerData() { - } - - // MARK: - Push metadata + func getServerData(forced: Bool = false) async { } - /// Pushes or reuses the folder view controller associated with the provided metadata. - /// - /// - Parameter metadata: The metadata representing the selected folder. - @MainActor - func pushMetadata(_ metadata: tableMetadata) async { - guard !isNavigatingMetadata, - let navigationController = self.navigationController, - let navigationCollectionViewCommon = self.controller?.navigationCollectionViewCommon else { + @objc func networkSearch() { + guard !networkSearchInProgress else { return } - - isNavigatingMetadata = true - defer { - isNavigatingMetadata = false + guard !session.account.isEmpty, + let literalSearch = literalSearch, + !literalSearch.isEmpty else { + return } + let capabilities = NCNetworking.shared.capabilities[session.account] ?? NKCapabilities.Capabilities() - let serverUrlPush = utilityFileSystem.createServerUrl( - serverUrl: metadata.serverUrl, - fileName: metadata.fileName - ) - - // Update the last opening date without blocking the main flow. - Task.detached(priority: .utility) { [database] in - await database.setDirectoryLastOpeningDateAsync(ocId: metadata.ocId) + self.networkSearchInProgress = true + self.dataSource.removeAll() + Task { + await self.reloadDataSource() } - guard navigationController.transitionCoordinator == nil else { - return + if capabilities.serverVersionMajor >= global.nextcloudVersion20 { + self.networking.unifiedSearchFiles(literal: literalSearch, account: session.account) { task in + self.searchDataSourceTask = task + Task { + await self.reloadDataSource() + } + } providers: { account, searchProviders in + self.providers = searchProviders + self.searchResults = [] + self.dataSource = NCCollectionViewDataSource(metadatas: [], + layoutForView: self.layoutForView, + providers: self.providers, + searchResults: self.searchResults, + account: account) + } update: { _, _, searchResult, metadatas in + guard let metadatas, !metadatas.isEmpty, self.isSearchingMode, let searchResult else { return } + self.networking.unifiedSearchQueue.addOperation(NCCollectionViewUnifiedSearch(collectionViewCommon: self, metadatas: metadatas, searchResult: searchResult)) + } completion: { _, _ in + Task { + await self.reloadDataSource() + } + self.networkSearchInProgress = false + } + } else { + self.networking.searchFiles(literal: literalSearch, account: session.account) { task in + self.searchDataSourceTask = task + Task { + await self.reloadDataSource() + } + } completion: { metadatasSearch, error in + Task { + guard let metadatasSearch, + error == .success, + self.isSearchingMode + else { + self.networkSearchInProgress = false + await self.reloadDataSource() + return + } + let ocId = metadatasSearch.map { $0.ocId } + let metadatas = await self.database.getMetadatasAsync(predicate: NSPredicate(format: "ocId IN %@", ocId), + withLayout: self.layoutForView, + withAccount: self.session.account) + + self.dataSource = NCCollectionViewDataSource(metadatas: metadatas, + layoutForView: self.layoutForView, + providers: self.providers, + searchResults: self.searchResults, + account: self.session.account) + self.networkSearchInProgress = false + await self.reloadDataSource() + } + } } + } - if let existingEntry = navigationCollectionViewCommon.first(where: { - $0.navigationController === navigationController && $0.serverUrl == serverUrlPush - }) { - let viewController = existingEntry.viewController + func unifiedSearchMore(metadataForSection: NCMetadataForSection?) { + guard let metadataForSection = metadataForSection, let lastSearchResult = metadataForSection.lastSearchResult, let cursor = lastSearchResult.cursor, let term = literalSearch else { return } - if navigationController.topViewController === viewController { - return - } + metadataForSection.unifiedSearchInProgress = true + self.collectionView?.reloadData() - if navigationController.viewControllers.contains(where: { $0 === viewController }) { - navigationController.popToViewController(viewController, animated: true) - return + self.networking.unifiedSearchFilesProvider(id: lastSearchResult.id, term: term, limit: 5, cursor: cursor, account: session.account) { task in + self.searchDataSourceTask = task + Task { + await self.reloadDataSource() + } + } completion: { _, searchResult, metadatas, error in + if error != .success { + Task {@MainActor in + await showErrorBanner( + controller: self.controller, + errorDescription: error.errorDescription, + errorCode: error.errorCode + ) + } } - navigationController.pushViewController(viewController, animated: true) - return + metadataForSection.unifiedSearchInProgress = false + guard let searchResult = searchResult, let metadatas = metadatas else { return } + self.dataSource.appendMetadatasToSection(metadatas, metadataForSection: metadataForSection, lastSearchResult: searchResult) + + DispatchQueue.main.async { + self.collectionView?.reloadData() + } } + } + + // MARK: - Push metadata - guard let viewController = UIStoryboard(name: "NCFiles", bundle: nil).instantiateInitialViewController() as? NCFiles else { + func pushMetadata(_ metadata: tableMetadata) { + guard let navigationCollectionViewCommon = self.controller?.navigationCollectionViewCommon else { return } + let serverUrlPush = utilityFileSystem.createServerUrl(serverUrl: metadata.serverUrl, fileName: metadata.fileName) - viewController.serverUrl = serverUrlPush - viewController.titlePreviusFolder = navigationItem.title - viewController.titleCurrentFolder = metadata.fileNameView + // Set Last Opening Date + Task { + await database.setDirectoryLastOpeningDateAsync(ocId: metadata.ocId) + } - navigationCollectionViewCommon.append( - NavigationCollectionViewCommon( - serverUrl: serverUrlPush, - navigationController: navigationController, - viewController: viewController - ) - ) + if let viewController = navigationCollectionViewCommon.first(where: { $0.navigationController == self.navigationController && $0.serverUrl == serverUrlPush})?.viewController, viewController.isViewLoaded { + navigationController?.pushViewController(viewController, animated: true) + } else { + if let viewController: NCFiles = UIStoryboard(name: "NCFiles", bundle: nil).instantiateInitialViewController() as? NCFiles { + viewController.serverUrl = serverUrlPush + viewController.titlePreviusFolder = navigationItem.title + viewController.titleCurrentFolder = metadata.fileNameView - navigationController.pushViewController(viewController, animated: true) - } - - func pushViewController(viewController: UIViewController) { - if pushed { return } + navigationCollectionViewCommon.append(NavigationCollectionViewCommon(serverUrl: serverUrlPush, navigationController: self.navigationController, viewController: viewController)) - pushed = true - navigationController?.pushViewController(viewController, animated: true) + navigationController?.pushViewController(viewController, animated: true) + } + } } func pushViewController(viewController: UIViewController) { @@ -2001,7 +1265,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS !results.isEmpty { return Array(results) } - return nil } func sizeForHeaderInSection(section: Int) -> CGSize { @@ -2010,12 +1273,12 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let isIphone = UIDevice.current.userInterfaceIdiom == .phone if self.dataSource.isEmpty() { - height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset, isHeaderMenuTransferViewEnabled: isHeaderMenuTransferViewEnabled() != nil) + height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset) } else if isEditMode || (isLandscape && isIphone) { return CGSize.zero } else { - let (heightHeaderCommands, heightHeaderRichWorkspace, heightHeaderSection) = getHeaderHeight(section: section) - height = heightHeaderCommands + heightHeaderRichWorkspace + heightHeaderSection + let (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) = getHeaderHeight(section: section) + height = heightHeaderRichWorkspace + heightHeaderRecommendations + heightHeaderSection } return CGSize(width: collectionView.frame.width, height: height) @@ -2024,137 +1287,25 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // MARK: - Footer size func sizeForFooterInSection(section: Int) -> CGSize { - let sections = dataSource.numberOfSections() - let metadataForSection = self.dataSource.getMetadataForSection(section) - let isPaginated = metadataForSection?.lastSearchResult?.isPaginated ?? false - let metadatasCount: Int = metadataForSection?.lastSearchResult?.entries.count ?? 0 - var size = CGSize(width: collectionView.frame.width, height: 0) - - if section == sections - 1 { - size.height += 85 - } else { - size.height += 1 - } - - if isSearchingMode && isPaginated && metadatasCount > 0 { - size.height += 30 - } - return size - } - - func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } -} - -extension NCCollectionViewCommon: NCSectionFirstHeaderDelegate { - func tapRichWorkspace(_ sender: Any) { - if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { - if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { - viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" - viewerRichWorkspace.serverUrl = serverUrl - viewerRichWorkspace.delegate = self - - navigationController.modalPresentationStyle = .fullScreen - self.present(navigationController, animated: true, completion: nil) - } - } - } - - func tapRecommendations(with metadata: tableMetadata) { - Task { - await didSelectMetadata(metadata, withOcIds: false) - } - } -} - -extension NCCollectionViewCommon: NCSectionFooterDelegate { - func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { - Task { - await unifiedSearchMore(metadataForSection: metadataForSection) + guard let controller else { + return CGSize.zero } - } -} - -// MARK: - Transfer Delegate - -extension NCCollectionViewCommon: NCTransferDelegate { - func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + let sections = dataSource.numberOfSections() + let bottomAreaInsets: CGFloat = controller.tabBar.safeAreaInsets.bottom == 0 ? 34 : 0 + let height = controller.tabBar.frame.height + bottomAreaInsets - func transferReloadData(serverUrl: String?) { - Task { - await self.debouncerReloadData.call({ - self.collectionView.reloadData() - }, immediate: true) + if isEditMode { + return CGSize(width: collectionView.frame.width, height: 90 + height) } - } - - func transferChange(status: String, - account: String, - fileName: String, - serverUrl: String, - selector: String?, - ocId: String, - destination: String?, - error: NKError) { - Task { - if error != .success, - error.errorCode != global.errorResourceNotFound { - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) - } - - guard session.account == account else { - return - } - - if self.isSearchingMode { - await self.debouncerNetworkSearch.call { - await self.search() - } - return - } - switch status { - case self.global.networkingStatusCreateFolder: - if error == .success, - serverUrl == self.serverUrl, - selector != self.global.selectorUploadAutoUpload, - let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { - if metadata.e2eEncrypted { - await self.reloadDataSource() - } else { - await self.pushMetadata(metadata) - } - } - default: - if self.serverUrl == serverUrl || destination == self.serverUrl || self.serverUrl.isEmpty { - await self.debouncerReloadDataSource.call { - await self.reloadDataSource() - } - } - } + if isSearchingMode { + return CGSize(width: collectionView.frame.width, height: 50) } - } - func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { - Task { - if self.isSearchingMode { - await self.debouncerNetworkSearch.call { - await self.search() - } - return - } - - if requestData && (self.serverUrl == serverUrl || serverUrl == nil) { - await self.debouncerGetServerData.call { - await self.getServerData() - } - return - } - - if self.serverUrl == serverUrl || serverUrl == nil { - await self.debouncerReloadDataSource.call { - await self.reloadDataSource() - } - } + if section == sections - 1 { + return CGSize(width: collectionView.frame.width, height: height) + } else { + return CGSize(width: collectionView.frame.width, height: 0) } } } diff --git a/iOSClient/NCBackgroundLocationUploadManager.swift b/iOSClient/NCBackgroundLocationUploadManager.swift index f010b147d3..3e82c5e4f5 100644 --- a/iOSClient/NCBackgroundLocationUploadManager.swift +++ b/iOSClient/NCBackgroundLocationUploadManager.swift @@ -1,6 +1,10 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2025 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCBackgroundLocationUploadManager.swift +// Nextcloud +// +// Created by Marino Faggiana on 06/06/25. +// Copyright © 2025 Marino Faggiana. All rights reserved. +// import CoreLocation import NextcloudKit @@ -102,16 +106,15 @@ class NCBackgroundLocationUploadManager: NSObject, CLLocationManagerDelegate { } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - // Must work only in background guard isAppInBackground else { return } // Open Realm - guard NCManageDatabase.shared.openRealmBackground() else { - nkLog(tag: self.global.logTagLocation, emoji: .error, message: "Failed to open Realm in Location Manager") - return - } + if database.openRealmBackground() { + let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! + let location = locations.last + nkLog(tag: self.global.logTagLocation, emoji: .start, message: "Triggered by location change: \(location?.coordinate.latitude ?? 0), \(location?.coordinate.longitude ?? 0)") let location = locations.last nkLog(tag: self.global.logTagLocation, emoji: .start, message: "Triggered by location change: \(location?.coordinate.latitude ?? 0), \(location?.coordinate.longitude ?? 0)") diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index 81f58bb5f2..ec0a3d2fb1 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -43,13 +43,14 @@ final class NCGlobal: Sendable { // Intro selector // let introLogin: Int = 0 + let introSignup: Int = 1 let introSignUpWithProvider: Int = 1 // Avatar // let avatarSize: Int = 128 * Int(UIScreen.main.scale) let avatarSizeRounded: Int = 128 - + // Preview size // let size1024: CGSize = CGSize(width: 1024, height: 1024) @@ -113,7 +114,35 @@ final class NCGlobal: Sendable { // let buttonMoreMore = "more" let buttonMoreLock = "moreLock" - + let buttonMoreStop = "stop" + + // Standard height sections header/footer + // + let heightButtonsView: CGFloat = 50 + let heightHeaderTransfer: CGFloat = 50 + let heightSection: CGFloat = 30 + let heightFooter: CGFloat = 1 + let heightFooterButton: CGFloat = 30 + let endHeightFooter: CGFloat = 85 + + + // Text - OnlyOffice - Collabora - QuickLook + // + let editorText = "text" + let editorOnlyoffice = "onlyoffice" + let editorCollabora = "collabora" + let editorQuickLook = "quicklook" + + let onlyofficeDocx = "onlyoffice_docx" + let onlyofficeXlsx = "onlyoffice_xlsx" + let onlyofficePptx = "onlyoffice_pptx" + + // Template + // + let templateDocument = "document" + let templateSpreadsheet = "spreadsheet" + let templatePresentation = "presentation" + // Rich Workspace // let fileNameRichWorkspace = "Readme.md" @@ -198,6 +227,8 @@ final class NCGlobal: Sendable { let selectorSaveAsScan = "saveAsScan" let selectorOpenDetail = "openDetail" let selectorSynchronizationOffline = "synchronizationOffline" + let selectorPrint = "print" + let selectorDeleteFile = "deleteFile" // Metadata : Status // @@ -228,7 +259,6 @@ final class NCGlobal: Sendable { let metadataStatusForScreenAwake = [-1, -2, 1, 2] let metadataStatusHideInView = [1, 2, 3, 11] let metadataStatusWaitWebDav = [10, 11, 12, 13, 14, 15] - let metadataStatusTransfers = [-2, -3, 2, 3, 10, 11, 12, 13, 14, 15] let metadatasStatusInWaiting = [-1, 1, 10, 11, 12, 13, 14, 15] let metadatasStatusInWaitingDownloadUpload = [-1, 1] @@ -246,6 +276,8 @@ final class NCGlobal: Sendable { let notificationCenterChangeTheming = "changeTheming" // userInfo: account let notificationCenterRichdocumentGrabFocus = "richdocumentGrabFocus" let notificationCenterReloadDataNCShare = "reloadDataNCShare" + let notificationCenterDidCreateShareLink = "didCreateShareLink" + let notificationCenterCloseRichWorkspaceWebView = "closeRichWorkspaceWebView" let notificationCenterReloadAvatar = "reloadAvatar" let notificationCenterClearCache = "clearCache" @@ -253,6 +285,8 @@ final class NCGlobal: Sendable { let notificationCenterServerDidUpdate = "serverDidUpdate" // userInfo: account let notificationCenterNetworkReachability = "networkReachability" + let notificationCenterRenameFile = "renameFile" // userInfo: serverUrl, account, error + let notificationCenterMenuSearchTextPDF = "menuSearchTextPDF" let notificationCenterMenuGotToPageInPDF = "menuGotToPageInPDF" @@ -269,6 +303,7 @@ final class NCGlobal: Sendable { let notificationCenterUserInteractionMonitor = "serInteractionMonitor" let notificationCenterNetworkingProcess = "networkingProcess" + let notificationCenterFavoriteStatusChanged = "favoriteStatusChanged" // Networking Status let networkingStatusCreateFolder = "statusCreateFolder" @@ -285,6 +320,7 @@ final class NCGlobal: Sendable { let networkingStatusUploaded = "statusUploaded" let networkingStatusReloadAvatar = "statusReloadAvatar" + let notificationCenterUpdateIcons = "updateIcons" // TIP // @@ -367,6 +403,20 @@ final class NCGlobal: Sendable { // let taskDescriptionRetrievesProperties = "retrievesProperties" let taskDescriptionSynchronization = "synchronization" + let taskDescriptionDeleteFileOrFolder = "deleteFileOrFolder" + + // MoEngage App Version + // + let moEngageAppVersion = 854 + + // Filename Mask and Type + // + let keyFileNameMask = "fileNameMask" + let keyFileNameType = "fileNameType" + let keyFileNameAutoUploadMask = "fileNameAutoUploadMask" + let keyFileNameAutoUploadType = "fileNameAutoUploadType" + let keyFileNameOriginal = "fileNameOriginal" + let keyFileNameOriginalAutoUpload = "fileNameOriginalAutoUpload" // LOG TAG // diff --git a/iOSClient/Networking/E2EE/NCNetworkingE2EEUpload.swift b/iOSClient/Networking/E2EE/NCNetworkingE2EEUpload.swift index f43a4fbcd5..5541fb7326 100644 --- a/iOSClient/Networking/E2EE/NCNetworkingE2EEUpload.swift +++ b/iOSClient/Networking/E2EE/NCNetworkingE2EEUpload.swift @@ -218,7 +218,7 @@ class NCNetworkingE2EEUpload: NSObject { await self.database.addMetadataAsync(metadata) await self.database.addLocalFilesAsync(metadatas: [metadata]) - + await self.database.addLocalFileAsync(metadata: metadata) utility.createImageFileFrom(metadata: metadata) await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in diff --git a/iOSClient/Networking/NCConfigServer.swift b/iOSClient/Networking/NCConfigServer.swift index f82b466647..d5e4d2a867 100644 --- a/iOSClient/Networking/NCConfigServer.swift +++ b/iOSClient/Networking/NCConfigServer.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2022 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCConfigServer.swift +// Nextcloud +// +// Created by Marino Faggiana on 05/12/22. +// Copyright © 2022 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import Foundation import UIKit diff --git a/iOSClient/Networking/NCNetworking+Download.swift b/iOSClient/Networking/NCNetworking+Download.swift index 4d9fe0c51c..22ecb91653 100644 --- a/iOSClient/Networking/NCNetworking+Download.swift +++ b/iOSClient/Networking/NCNetworking+Download.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCNetworking+Download.swift +// Nextcloud +// +// Created by Marino Faggiana on 07/02/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import UIKit import NextcloudKit @@ -327,3 +346,40 @@ extension NCNetworking { } } } + +class NCOperationDownload: ConcurrentOperation, @unchecked Sendable { + var metadata: tableMetadata + var selector: String + + init(metadata: tableMetadata, selector: String) { + self.metadata = tableMetadata.init(value: metadata) + self.selector = selector + } + + override func start() { + guard !isCancelled else { return self.finish() } + + metadata.session = NCNetworking.shared.sessionDownload + metadata.sessionError = "" + metadata.sessionSelector = selector + metadata.sessionTaskIdentifier = 0 + metadata.status = NCGlobal.shared.metadataStatusWaitDownload + +// let metadata = NCManageDatabase.shared.addMetadata(metadata) + +// NCNetworking.shared.download(metadata: metadata, withNotificationProgressTask: true) { +// } completion: { _, _ in +// self.finish() +// } + Task { + await download(withSelector: self.selector) + } + } + + private func download(withSelector selector: String = "") async { + await NCNetworking.shared.downloadFile(metadata: metadata) { _ in + self.finish() + } taskHandler: { _ in } + + } +} diff --git a/iOSClient/Networking/NCNetworking+LivePhoto.swift b/iOSClient/Networking/NCNetworking+LivePhoto.swift index e861b76b81..072b3b087f 100644 --- a/iOSClient/Networking/NCNetworking+LivePhoto.swift +++ b/iOSClient/Networking/NCNetworking+LivePhoto.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCNetworking+LivePhoto.swift +// Nextcloud +// +// Created by Marino Faggiana on 07/02/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import UIKit import NextcloudKit diff --git a/iOSClient/Networking/NCNetworking+Synchronization.swift b/iOSClient/Networking/NCNetworking+Synchronization.swift new file mode 100644 index 0000000000..270c19864b --- /dev/null +++ b/iOSClient/Networking/NCNetworking+Synchronization.swift @@ -0,0 +1,93 @@ +// +// NCNetworking+Synchronization.swift +// Nextcloud +// +// Created by Marino Faggiana on 07/02/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import UIKit +import NextcloudKit + +extension NCNetworking { + internal func synchronization(account: String, serverUrl: String, userId: String, urlBase: String, metadatasInDownload: [tableMetadata]?) async { + let showHiddenFiles = NCPreferences().getShowHiddenFiles(account: account) + let options = NKRequestOptions(timeout: 300, taskDescription: NCGlobal.shared.taskDescriptionSynchronization, queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue) + + nkLog(tag: self.global.logTagSync, emoji: .start, message: "Start read infinite folder: \(serverUrl)") + + let results = await NextcloudKit.shared.readFileOrFolderAsync(serverUrlFileName: serverUrl, depth: "infinity", showHiddenFiles: showHiddenFiles, account: account, options: options) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, + path: serverUrl, + name: "readFileOrFolder") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } + + if results.error == .success, let files = results.files { + nkLog(tag: self.global.logTagSync, emoji: .success, message: "Read infinite folder: \(serverUrl)") + + for file in files { + if file.directory { + let metadata = await NCManageDatabase.shared.convertFileToMetadataAsync(file) + await NCManageDatabase.shared.createDirectory(metadata: metadata) + } else { + if await isFileDifferent(ocId: file.ocId, fileName: file.fileName, etag: file.etag, metadatasInDownload: metadatasInDownload, userId: userId, urlBase: urlBase) { + let metadata = await NCManageDatabase.shared.convertFileToMetadataAsync(file) + metadata.session = self.sessionDownloadBackground + metadata.sessionSelector = NCGlobal.shared.selectorSynchronizationOffline + metadata.sessionTaskIdentifier = 0 + metadata.sessionError = "" + metadata.status = NCGlobal.shared.metadataStatusWaitDownload + metadata.sessionDate = Date() + + await NCManageDatabase.shared.addMetadataAsync(metadata) + + nkLog(tag: self.global.logTagSync, emoji: .start, message: "File download: \(file.serverUrl)/\(file.fileName)") + } + } + } + } else { + nkLog(tag: self.global.logTagSync, emoji: .error, message: "Read infinite folder: \(serverUrl), error: \(results.error.errorCode)") + } + + nkLog(tag: self.global.logTagSync, emoji: .stop, message: "Stop read infinite folder: \(serverUrl)") + } + + internal func isFileDifferent(ocId: String, + fileName: String, + etag: String, + metadatasInDownload: [tableMetadata]?, + userId: String, + urlBase: String) async -> Bool { + let match = metadatasInDownload?.contains { $0.ocId == ocId } ?? false + if match { + return false + } + + guard let localFile = await NCManageDatabase.shared.getTableLocalFileAsync(predicate: NSPredicate(format: "ocId == %@", ocId)) else { + return true + } + let fileNamePath = self.utilityFileSystem.getDirectoryProviderStorageOcId(ocId, fileName: fileName, userId: userId, urlBase: urlBase) + let size = await self.utilityFileSystem.fileSizeAsync(atPath: fileNamePath) + let isDifferent = (localFile.etag != etag) || size == 0 + + return isDifferent + } +} diff --git a/iOSClient/Networking/NCNetworking+Task.swift b/iOSClient/Networking/NCNetworking+Task.swift index f28830b583..1b6122252b 100644 --- a/iOSClient/Networking/NCNetworking+Task.swift +++ b/iOSClient/Networking/NCNetworking+Task.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCNetworking+Task.swift +// Nextcloud +// +// Created by Marino Faggiana on 24/08/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import Foundation import UIKit diff --git a/iOSClient/Networking/NCNetworking+Upload.swift b/iOSClient/Networking/NCNetworking+Upload.swift index 3bd13a4c56..72bbffdf21 100644 --- a/iOSClient/Networking/NCNetworking+Upload.swift +++ b/iOSClient/Networking/NCNetworking+Upload.swift @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later - import UIKit import NextcloudKit import Alamofire @@ -45,6 +41,22 @@ extension NCNetworking { } taskHandler(task) } progressHandler: { progress in + Task { + guard await self.progressQuantizer.shouldEmit(serverUrlFileName: serverUrlFileName, fraction: progress.fractionCompleted) else { + return + } + + if let metadata { + await NCManageDatabase.shared.setMetadataProgress(ocId: metadata.ocId, progress: progress.fractionCompleted) + await self.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferProgressDidUpdate(progress: Float(progress.fractionCompleted), + totalBytes: progress.totalUnitCount, + totalBytesExpected: progress.completedUnitCount, + fileName: metadata.fileName, + serverUrl: metadata.serverUrl) + } + } + } progressHandler(progress.completedUnitCount, progress.totalUnitCount, progress.fractionCompleted) } @@ -374,6 +386,7 @@ extension NCNetworking { error: self.global.diagnosticProblemsUploadServerError) } } + await NCManageDatabase.shared.updateBadge() } // MARK: - diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 9671a57355..6eabc1aa6b 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCNetworking+WebDAV.swift +// Nextcloud +// +// Created by Marino Faggiana on 07/02/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import UIKit import NextcloudKit @@ -465,6 +484,50 @@ extension NCNetworking { return .success } + + func renameMetadata(_ metadata: tableMetadata, + fileNameNew: String, + indexPath: IndexPath, + viewController: UIViewController?, + completion: @escaping (_ error: NKError) -> Void) { + + let permission = NCMetadataPermissions.permissionsContainsString(metadata.permissions, permissions: NCMetadataPermissions.permissionCanRename) + if (!metadata.permissions.isEmpty && permission == false) || + (metadata.status != global.metadataStatusNormal && metadata.status != global.metadataStatusWaitRename) { +// return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_no_permission_modify_file_")) +// let error = await NCNetworkingE2EERename().rename(metadata: metadata, fileNameNew: fileNameNew) +// DispatchQueue.main.async { completion(error) } + completion(NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_no_permission_modify_file_")) + } + + if metadata.isDirectoryE2EE { +#if !EXTENSION + if isOffline { +// return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) + completion(NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) + } + Task { + let error = await NCNetworkingE2EERename().rename(metadata: metadata, fileNameNew: fileNameNew) + if error != .success { +// NCContentPresenter().showError(error: error) + completion(error) + } + } +#endif + } else { + Task { + await self.transferDispatcher.notifyAllDelegatesAsync { delegate in + let status = self.global.metadataStatusWaitRename + await NCManageDatabase.shared.renameMetadata(fileNameNew: fileNameNew, ocId: metadata.ocId, status: status) + delegate.transferReloadData(serverUrl: metadata.serverUrl, status: status) + } + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterRenameFile, userInfo: ["serverUrl": metadata.serverUrl, "account": metadata.account, "error": NKError(errorCode: 0, errorDescription: ""), "ocId": metadata.ocId, "indexPath": indexPath]) + + } + + completion(NKError(errorCode: 0, errorDescription: "")) + } + } func renameFileOrFolder(metadata: tableMetadata) async -> NKError { let serverUrlFileNameSource = metadata.serverUrlFileName diff --git a/iOSClient/Networking/NCNetworking.swift b/iOSClient/Networking/NCNetworking.swift index 516b0e64e1..7868b64db4 100644 --- a/iOSClient/Networking/NCNetworking.swift +++ b/iOSClient/Networking/NCNetworking.swift @@ -37,6 +37,203 @@ protocol NCTransferDelegate: AnyObject { serverUrl: String) } +extension NCTransferDelegate { + func transferChange(status: String, + account: String, + fileName: String, + serverUrl: String, + selector: String?, + ocId: String, + destination: String?, + error: NKError) {} + func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) {} + func transferProgressDidUpdate(progress: Float, + totalBytes: Int64, + totalBytesExpected: Int64, + fileName: String, + serverUrl: String) {} +} + +/// Actor-based delegate dispatcher using weak references. +actor NCTransferDelegateDispatcher { + // Weak reference collection of delegates + private var transferDelegates = NSHashTable.weakObjects() + + /// Adds a delegate safely. + func addDelegate(_ delegate: NCTransferDelegate) { + transferDelegates.add(delegate) + } + + /// Remove a delegate safely. + func removeDelegate(_ delegate: NCTransferDelegate) { + transferDelegates.remove(delegate) + } + + /// Notifies all delegates. + func notifyAllDelegates(_ block: (NCTransferDelegate) -> Void) { + let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + for delegate in delegatesCopy { + block(delegate) + } + } + + func notifyAllDelegatesAsync(_ block: @escaping (NCTransferDelegate) async -> Void) async { + let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + for delegate in delegatesCopy { + await block(delegate) + } + } + + /// Notifies the delegate for a specific scene. + func notifyDelegate(forScene sceneIdentifier: String, _ block: (NCTransferDelegate) -> Void) { + let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + for delegate in delegatesCopy { + if delegate.sceneIdentifier == sceneIdentifier { + block(delegate) + } + } + } + + /// Notifies matching and non-matching delegates for a specific scene. + func notifyDelegates(forScene sceneIdentifier: String, + matching: (NCTransferDelegate) -> Void, + others: (NCTransferDelegate) -> Void) { + let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + for delegate in delegatesCopy { + if delegate.sceneIdentifier == sceneIdentifier { + matching(delegate) + } else { + others(delegate) + } + } + } +} + +/// A thread-safe registry for tracking in-flight `URLSessionTask` instances. +/// +/// Each task is associated with a string identifier (`identifier`) that you define, +/// allowing you to check whether a request is already running, avoid duplicates, +/// and cancel all active tasks at once. The registry automatically removes +/// completed tasks via `cleanupCompleted()` to keep memory usage compact. +/// +/// Typical use cases: +/// - Ensure only one task per identifier is active at a time. +/// - Query whether a specific request is still running (`isReading`). +/// - Forcefully stop a specific request (`cancel`). +/// - Forcefully stop all tasks when leaving a screen (`cancelAll`). +actor NetworkingTasks { + private var active: [(identifier: String, task: URLSessionTask)] = [] + + /// Returns whether there is an in-flight task for the given URL. + /// + /// A task is considered in-flight if its `state` is `.running` or `.suspended`. + /// - Parameter identifier: The identifier to check. + /// - Returns: `true` if a matching in-flight task exists; otherwise `false`. + func isReading(identifier: String) -> Bool { + // Drop finished/canceling tasks globally + cleanup() + + return active.contains { + $0.identifier == identifier && ($0.task.state == .running || $0.task.state == .suspended) + } + } + + /// Tracks a newly created `URLSessionTask` for the given identifier. + /// + /// If a running entry for the same identifier exists, it is removed before appending the new one. + /// - Parameters: + /// - identifier: The identifier associated with the task. + /// - task: The `URLSessionTask` to track. + func track(identifier: String, task: URLSessionTask) { + // Drop finished/canceling tasks globally + cleanup() + + active.removeAll { + $0.identifier == identifier && $0.task.state == .running + } + active.append((identifier, task)) + nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .start, message: "Start task for identifier: \(identifier)", consoleOnly: true) + } + + /// create a Identifier + /// + func createIdentifier(account: String? = nil, path: String? = nil, name: String) -> String { + if let account, + let path { + return account + "_" + path + "_" + name + } else if let path { + return path + "_" + name + } else { + return name + } + } + + /// Cancels and removes all tasks associated with the given id. + /// + /// - Parameter identifier: The identifier whose tasks should be canceled. + func cancel(identifier: String) { + // Drop finished/canceling tasks globally + cleanup() + + for element in active where element.identifier == identifier { + element.task.cancel() + nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .cancel, message: "Cancel task for identifier: \(identifier)", consoleOnly: true) + } + active.removeAll { + $0.identifier == identifier + } + } + + /// Cancels all tracked `URLSessionTask` and clears the registry. + /// + /// Call this when leaving the page/screen or when the operation must be forcefully stopped. + func cancelAll() { + active.forEach { + $0.task.cancel() + nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .cancel, message: "Cancel task with identifier: \($0.identifier)", consoleOnly: true) + } + active.removeAll() + } + + /// Removes tasks that have completed from the registry. + /// + /// Useful to keep the in-memory list compact during long-running operations. + func cleanup() { + active.removeAll { + $0.task.state == .completed || $0.task.state == .canceling + } + } +} + +/// Quantizes per-task progress updates to integer percentages (0...100). +/// Each (serverUrlFileName) pair is tracked separately, so you get +/// at most one update per integer percent for each transfer. +actor ProgressQuantizer { + private var lastPercent: [String: Int] = [:] + + /// Returns `true` only when integer percent changes (or hits 100). + /// + /// - Parameters: + /// - serverUrlFileName: The name of the file being transferred. + /// - fraction: Progress fraction [0.0 ... 1.0]. + func shouldEmit(serverUrlFileName: String, fraction: Double) -> Bool { + let percent = min(max(Int((fraction * 100).rounded(.down)), 0), 100) + + let last = lastPercent[serverUrlFileName] ?? -1 + guard percent != last || percent == 100 else { + return false + } + + lastPercent[serverUrlFileName] = percent + return true + } + + /// Clears stored state for a finished transfer. + func clear(serverUrlFileName: String) { + lastPercent.removeValue(forKey: serverUrlFileName) + } +} + class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { static let shared = NCNetworking() @@ -93,6 +290,7 @@ class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { let saveLivePhotoQueue = Queuer(name: "saveLivePhotoQueue", maxConcurrentOperationCount: 1, qualityOfService: .default) let downloadAvatarQueue = Queuer(name: "downloadAvatarQueue", maxConcurrentOperationCount: 10, qualityOfService: .default) #endif + let downloadQueue = Queuer(name: "downloadQueue", maxConcurrentOperationCount: NCBrandOptions.shared.httpMaximumConnectionsPerHostInDownload, qualityOfService: .default) // MARK: - init diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index 930a965320..9121911b0b 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -271,7 +271,6 @@ actor NCNetworkingProcess { await startTimer(interval: minInterval) } } else { - // Remove upload asset await removeUploadedAssetsIfNeeded() // Set Live Photo diff --git a/iOSClient/Networking/NCService.swift b/iOSClient/Networking/NCService.swift index f6715693d3..2987e2543c 100644 --- a/iOSClient/Networking/NCService.swift +++ b/iOSClient/Networking/NCService.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2018 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCService.swift +// Nextcloud +// +// Created by Marino Faggiana on 14/03/18. +// Copyright © 2018 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import UIKit @preconcurrency import NextcloudKit diff --git a/iOSClient/Settings/AnalysisDataCollectionSwitch.swift b/iOSClient/Settings/AnalysisDataCollectionSwitch.swift index e24216220f..58f2d55d8d 100644 --- a/iOSClient/Settings/AnalysisDataCollectionSwitch.swift +++ b/iOSClient/Settings/AnalysisDataCollectionSwitch.swift @@ -7,6 +7,7 @@ // import UIKit +import XLForm class AnalysisDataCollectionSwitch: XLFormBaseCell { diff --git a/iOSClient/Settings/InitialPrivacySettingsViewController.swift b/iOSClient/Settings/InitialPrivacySettingsViewController.swift index 2cd30a4242..8ae7b2e186 100644 --- a/iOSClient/Settings/InitialPrivacySettingsViewController.swift +++ b/iOSClient/Settings/InitialPrivacySettingsViewController.swift @@ -10,6 +10,7 @@ import Foundation import AppTrackingTransparency import AdSupport import UIKit +import XLForm class InitialPrivacySettingsViewController: UIViewController { diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index 5e56a6bd8d..b3993620ba 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -9,6 +9,7 @@ import Foundation import AppTrackingTransparency import AdSupport +import XLForm class PrivacySettingsViewController: XLFormViewController{ diff --git a/iOSClient/Settings/RequiredDataCollectionSwitch.swift b/iOSClient/Settings/RequiredDataCollectionSwitch.swift index f80aa97e89..0d5a34732f 100644 --- a/iOSClient/Settings/RequiredDataCollectionSwitch.swift +++ b/iOSClient/Settings/RequiredDataCollectionSwitch.swift @@ -7,7 +7,7 @@ // import UIKit - +import XLForm class RequiredDataCollectionSwitch: XLFormBaseCell { diff --git a/iOSClient/Settings/SaveSettingsCustomButtonCell.swift b/iOSClient/Settings/SaveSettingsCustomButtonCell.swift index 6205727723..1332b1f92a 100644 --- a/iOSClient/Settings/SaveSettingsCustomButtonCell.swift +++ b/iOSClient/Settings/SaveSettingsCustomButtonCell.swift @@ -7,6 +7,7 @@ // import UIKit +import XLForm class SaveSettingsCustomButtonCell: XLFormButtonCell { diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 822dd4e1b1..8be6601682 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -20,6 +20,7 @@ // along with this program. If not, see . // +"_itunes_" = "iTunes"; "_cancel_" = "Cancel"; "_edit_" = "Edit"; "_tap_to_cancel_" = "Tap to cancel"; @@ -27,60 +28,118 @@ "_cancel_request_" = "Do you want to cancel?"; "_upload_file_" = "Upload file"; "_download_file_" = "Download file"; +"_loading_" = "Loading"; +"_loading_with_points_" = "Loading …"; +"_loading_num_" = "Loading file %i"; +"_loading_autoupload_" = "Auto uploading"; +"_uploading_" = "Uploading"; +"_synchronization_" = "Synchronization"; "_delete_" = "Delete"; +"_delete_file_n_" = "Delete file %i of %i"; "_rename_" = "Rename"; "_rename_file_" = "Rename file"; "_rename_folder_" = "Rename folder"; "_move_" = "Move"; +"_move_file_n_" = "Move file %i of %i"; +"_creating_sharing_" = "Creating share"; +"_updating_sharing_" = "Updating share"; +"_removing_sharing_" = "Removing share"; "_add_" = "Add"; +"_login_" = "Log in"; "_save_" = "Save"; "_warning_" = "Warning"; "_error_" = "Error"; "_no_" = "No"; "_yes_" = "Yes"; "_select_" = "Select"; +"_deselect_" = "Deselect"; "_select_all_" = "Select all"; "_upload_" = "Upload"; "_home_" = "Files"; "_files_" = "Files"; +//"_files_" = "Files"; +"_home_dir_" = "Home"; +"_file_to_upload_" = "File to upload"; +"_destination_" = "Destination"; "_ok_" = "OK"; +"_beta_version_" = "Beta version"; +"_function_in_testing_" = "Function in testing, please send information about any problems you run into."; "_done_" = "Done"; "_clear_" = "Clear"; +"_passcode_too_short_" = "Passcode too short, at least 4 characters required"; "_selected_" = "Selected"; +"_scan_fingerprint_" = "Scan fingerprint to authenticate"; "_no_active_account_" = "No account found"; "_info_" = "Info"; "_warning_" = "Warning"; +"_email_" = "Email"; +"_save_exit_" = "Do you want to exit without saving?"; "_video_" = "Video"; "_overwrite_" = "Overwrite"; +"_transfers_in_queue_" = "Transfers in progress, please wait …"; +"_too_errors_upload_" = "Too many errors, please verify the problem"; "_create_" = "Create"; "_create_folder_" = "Create folder"; "_create_folder_e2ee_" = "Create encrypted folder"; +"_create_folder_on_" = "Create folder on"; "_close_" = "Close"; +"_postpone_" = "Postpone"; "_remove_" = "Remove"; "_file_not_found_" = "File not found"; "_continue_" = "Continue"; "_continue_editing_" = "Continue editing"; +"_continue_request_" = "Do you want to continue?"; "_auto_upload_folder_" = "Auto upload"; +"_gallery_" = "Gallery"; "_photo_" = "Photo"; "_audio_" = "Audio"; "_unknown_" = "Unknown"; +"_additional_view_options_" = "Additional view options"; +"_next_" = "Next"; "_success_" = "Success"; +"_initialization_" = "Initialization"; +"_experimental_" = "Experimental"; +"_select_dir_media_tab_" = "Select as folder \"Media\""; +"_error_creation_file_" = "Oops! Could not create the file"; +"_save_path_" = "Storage path"; +"_save_settings_" = "Save settings"; "_mode_filename_" = "Filename mode"; "_warning_owncloud_" = "You are connected to an ownCloud server. This is untested and unsupported, use at your own risk. To upgrade to Nextcloud, see https://nextcloud.com/migration."; "_warning_unsupported_" = "You are using an unsupported version of Nextcloud. For the safety of your data we strongly recommend updating to the latest stable Nextcloud."; "_restore_" = "Restore"; +"_camera_roll_" = "Camera roll"; "_tap_here_to_change_" = "Tap here to change"; "_no_albums_" = "No albums"; "_denied_album_" = "This app does not have access to \"Photos\". You can enable access in Privacy Settings."; "_denied_camera_" = "This app does not have access to the \"Camera\". You can enable access in Privacy Settings."; +"_start_" = "Start"; "_force_start_" = "Force the start"; "_account_does_not_exist_" = "The account %@ does not exist. Please log in again."; +//"_account_not_available_" = "The account %@ of %@ does not exist, please add it to be able to read the file %@."; +"_the_account_" = "The account"; +"_does_not_exist_" = "does not exist"; +"_purchase_" = "Purchase"; +"_account_not_exists_" = "The account %@ of %@ does not exist"; +"_account_not_available_" = "The account %@ of %@ does not exist, please add it to be able to read the file %@"; +"_the_account_" = "The account"; +"_does_not_exist_" = "does not exist"; +"_error_parameter_schema_" = "Wrong parameters, impossible to continue"; +"_comments_" = "Comments"; "_sharing_" = "Sharing"; +//"_details_" = "Share"; "_details_" = "Details"; +"_sub_details_" = "Subscription Details"; +"_subscriptions_" = "Subscriptions"; +"_dark_mode_" = "Dark mode"; +"_dark_mode_detect_" = "Detect iOS dark mode"; +"_screen_" = "Screen"; +"_wipe_account_" = "Account is wiped from server"; +"_appconfig_view_title_" = "Account configuration in progress …"; "_no_permission_add_file_" = "You don't have permission to add files."; "_no_permission_delete_file_" = "You don't have permission to delete files."; "_no_permission_modify_file_" = "You don't have permission to modify files."; "_no_permission_favorite_file_" = "You don't have permission to add the file to your favorites."; +"_request_upload_new_ver_" = "The file has been modified, do you want to upload the new version?"; "_add_folder_info_" = "Add folder info"; "_back_" = "Back"; "_search_" = "Search"; @@ -92,6 +151,8 @@ "_edit_tags_" = "Manage tags"; "_create_tag_error_" = "New tag could not be created. Please try again."; "_of_" = "of"; +"_internal_modify_" = "Edit with internal editor"; +"_database_corrupt_" = "Oops something went wrong, please enter your credentials but don't worry, your files have remained secure"; "_livephoto_save_" = "Save Live Photo to Photo Album"; "_livephoto_save_error_" = "Error during save of Live Photo."; "_livephoto_no_" = "Disable Live Photo"; @@ -107,9 +168,14 @@ "_copy_" = "Copy"; "_now_" = "Now"; "_wait_" = "Please wait …"; +"_attention_" = "Attention"; "_recent_" = "Recent"; "_view_in_folder_" = "View in folder"; "_leave_share_" = "Leave this share"; +"_premium_" = "Premium"; +"_professional_" = "Professional"; +"_current_" = "Current"; +"_buy_" = "Buy"; "_disabled_" = "Disabled"; "_compact_" = "Compact"; "_normal_" = "Normal"; @@ -139,24 +205,50 @@ "_locked_by_" = "Locked by %@"; "_file_locked_no_override_" = "This file is locked. It cannot be overridden."; "_lock_no_permissions_selected_" = "Not allowed for some selected files or folders."; +/* Remove a file from a list, don't delete it entirely */ "_remove_file_" = "Remove file"; + +/* Delete file and put it into the trash */ "_delete_file_" = "Delete file"; "_delete_folder_" = "Delete folder"; +"_delete_photo_" = "Delete photo"; +"_delete_video_" = "Delete video"; +"_automatic_Download_Image_" = "Use images in full resolution"; +"_automatic_Download_Image_footer_" = "When viewing images always download, if not available locally, the images in full resolution"; "_size_" = "Size"; -"_set_user_status_" = "Online status"; +//"_set_user_status_" = "Online status"; "_set_user_status_message_" = "Status message"; +"_file_size_" = "Exported file size"; +"_dimension_" = "Dimension"; +"_duration_" = "Duration"; +"_model_" = "Model"; +"_set_user_status_" = "Set user status"; "_open_settings_" = "Open settings"; +"_rename_ext_title_" = "Change file type?"; +"_rename_ext_message_" = "This file may behave differently if you change it from .%@ to %@"; +"_use_" = "Use"; +"_keep_" = "Keep"; +"_account_request_" = "Request account"; "_settings_account_request_" = "Request account at startup"; +"_print_" = "Print"; "_alias_" = "Alias"; -"_alias_placeholder_" = "Write alias"; -"_alias_footer_" = "Give your accounts a descriptive name such as Home, Office, School…"; +"_alias_placeholder_" = "Write the alias"; +"_alias_footer_" = "Give your account names a descriptive name such as Home, Office, School …"; +"_chunk_size_mb_" = "Chunk size in MB"; +"_chunk_footer_title_" = "Chunked file upload (0 is disabled)\nImportant: the chunked upload works only when the app is \"active\"."; "_privacy_legal_" = "Privacy and Legal Policy"; "_source_code_" = "Get source code"; "_account_select_" = "Select the account"; "_account_select_to_add_" = "Select the account to add"; +"_host_insert_" = "Insert the host name, for example:"; +"_certificate_not_found_" = "File %@ in documents directory not found."; +"_copy_failed_" = "Copy failed"; +"_certificate_installed_" = "Certificate installed"; "_remove_local_account_" = "Remove local account"; "_want_delete_account_" = "Do you want to remove local account?"; "_prevent_http_redirection_"= "The redirection in HTTP is not permitted."; +"_pdf_vertical_" = "PDF vertical display"; +"_pdf_horizontal_" = "PDF horizontal display"; "_single_file_conflict_title_" = "File conflict"; "_multi_file_conflict_title_" = "%@ File conflicts"; "_replace_action_title_" = "Replace"; @@ -174,6 +266,33 @@ "_change_lock_passcode_" = "Change passcode"; "_lock_cannot_disable_mdm_" = "Disabling the passcode lock is not permitted by your configuration profile."; +//TOPasscodeSettingsViewController.m +"_enter_passcode_" = "Enter Passcode"; +"_passcodes_didnt_match_try_again_" = "Passcodes didn't match. Try again."; +"_passcode_options_" = "Passcode Options"; +//"_next_" = "Next"; +"_enter_your_passcode_" = "Enter your passcode"; +"_enter_a_new_passcode_" = "Enter a new passcode"; +"_confirm_new_passcode_" = "Confirm new passcode"; +"_4_digit_numeric_code_" = "4-Digit Numeric Code"; +"_6_digit_numeric_code_" = "6-Digit Numeric Code"; +"_custom_numeric_code_" = "Custom Numeric Code"; +"_custom_alphanumeric_code_" = "Custom Alphanumeric Code"; + +//TOPasscodeSettingsWarningLabel.m +"_1_failed_passcode_attempt" = "1 Failed Passcode Attempt"; +"_failed_passcode_attempts" = "%d Failed Passcode Attempts"; + +/* Background of the file listing view */ +"_use_as_background_" = "Use it as a background"; + +/* Background of the file listing view */ +"_background_" = "Background"; + +"_dark_mode_" = "Dark mode"; +"_default_color_" = "Use the default color"; +"_as_default_color_" = "Use as default color"; + // MARK: User Status /* User status */ @@ -220,50 +339,149 @@ "_hours_" = "Hours"; "_minutes_" = "Minutes"; - +"_network_available_" = "Network available."; "_network_not_available_" = "Network unavailable."; +"_file_too_big_" = "File too large to be encrypted/decrypted"; +"_file_too_big_max_100_" = "File too large (max 100 kb.)"; +"_...loading..._" = "Loading …"; +"_download_plist_" = " "; +"_no_reuploadfile_" = "Could not find nor resend file. Delete the upload and reload the file to upload it."; "_file_already_exists_" = "Unable to complete the operation, a file with the same name exists."; +//"_file_already_exists_" = "Unable to complete the operation, a file with the same name exists."; +"_read_file_error_" = "Could not read the file"; +"_write_file_error_" = "Could not write the file"; "_files_lock_error_" = "There was an error changing the lock of this file."; "_more_" = "More"; "_notifications_" = "Notifications"; +"_logout_" = "Log out"; "_quota_space_unlimited_" = "Unlimited"; "_quota_space_unknown_" = "Unknown"; "_quota_using_" = "You are using %@ of %@"; +//"_quota_using_" = "%@ "; +"_quota_using_of_" = "of %@"; +"_quota_using_percentage_" = "Memory to %@ occupied"; "_acknowledgements_" = "Acknowledgements"; "_settings_" = "Settings"; +"_passcode_" = "Password"; "_enter_password_" = "Enter password …"; "_lock_" = "Lock"; "_lock_active_" = "Lock: On"; "_lock_not_active_" = "Lock: Off"; "_lock_protection_no_screen_" = "Do not ask at startup"; +"_lock_protection_no_screen_footer_" = "Use \"Do not ask at startup\" for the encryption option."; "_enable_touch_face_id_" = "Enable Touch/Face ID"; +"_face_id_" = "Face ID"; +"_touch_id_" = "Touch ID"; +"_security_" = "Security"; +"_data_protection_" = "Data protection"; +"_privacy_settings_" = "Privacy Settings"; +"_used_opensource_software_" = "OpenSource software used"; +"_service_" = "Service"; +"_imprint_" = "Imprint"; +"_magentacloud_version_" = "MagentaCLOUD Version"; +"_your_secure_cloud_storage_" = "Your secure cloud storage"; +"_url_" = "URL"; +"_username_" = "Username"; +"_change_credentials_" = "Change your credentials"; "_wifi_only_" = "Only use Wi-Fi connection"; "_settings_autoupload_" = "Auto upload photos"; +//"_settings_autoupload_" = "Auto upload"; +"_app_version_" = "Application version"; +"_app_in_use_" = "Application in use"; +"_contact_by_email_" = "Contact us by email"; "_clear_cache_" = "Clear cache"; "_clear_cache_footer_" = "Clear downloaded and offline files from the app"; -"_exit_" = "Reset application"; +"_exit_" = "Logout"; "_exit_footer_" = "Remove all accounts and local data from the app."; +"_funct_not_enabled_" = "Functionality not enabled"; +"_passcode_activate_" = "Password lock on"; +"_disabling_passcode_" = "Removing password lock"; "_want_exit_" = "Attention! Will be reset to initial state. Continue?"; +"_proceed_" = "Proceed"; +"_delete_cache_" = "Delete cache"; "_want_delete_cache_" = "Do you want to delete the cache (this also removes the transfers in progress)?"; +"_want_delete_thumbnails_" = "Do you want to delete all thumbnails too?"; +"_mail_deleted_" = "Email deleted"; +"_mail_saved_" = "Email saved"; +"_mail_sent_" = "Email sent"; +"_mail_failure_" = "Could not send email: %@"; +"_information_req_" = "Information request"; +"_write_in_english_" = "Kindly write to us in English"; +"_credentials_" = "Credentials"; +"_manage_account_" = "Manage account"; +"_change_password_" = "Change password"; "_add_account_" = "Add account"; -"_want_delete_" = "You will delete the following: "; +//"_want_delete_" = "You will delete the following: "; +//"_want_leave_share_" = "You will leave the following shares: "; +"_delete_account_" = "Remove account"; +"_delete_active_account_" = "Remove active account"; +"_want_delete_" = "Do you really want to delete?"; +"_want_leave_share_" = "You will leave the following shares: "; +"_delete_account_" = "Remove account"; +"_delete_active_account_" = "Remove active account"; +"_want_delete_" = "Do you really want to delete?"; "_want_leave_share_" = "You will leave the following shares: "; "_no_delete_" = "No, do not delete"; "_yes_delete_" = "Yes, delete"; +"_remove_cache_" = "Deleting cache, please wait …"; +"_optimizations_" = "Optimizations"; +"_synchronizations_" = "Synchronized folders"; +"_version_server_" = "Server version"; +"_help_" = "Help"; +"_change_simply_passcode_" = "Change password type"; +"_quota_" = "Quota"; +"_available_" = "Available"; +"_not_available_" = "Not available"; +"_accounts_" = "Accounts"; "_information_" = "Information"; +"_personal_information_" = "Personal info"; +"_user_full_name_" = "Full name"; +"_user_address_" = "Address"; +"_user_phone_" = "Phone number"; +"_user_email_" = "Email"; +"_user_web_" = "Website"; +"_user_twitter_" = "Twitter"; +"_user_job_" = "Job"; +"_user_businesssize_" = "Business size"; +"_user_businesstype_" = "Business type"; +"_user_city_" = "City"; +"_user_country_" = "Country"; +"_user_company_" = "Company"; +"_user_role_" = "Role"; +"_user_zip_" = "Zip"; +"_user_owner_" = "Owner"; +"_user_employee_" = "Employee"; +"_user_contractor_" = "Contractor"; +"_user_editprofile_" = "Edit profile"; "_select_offline_warning_" = "Making multiple files and folders available offline may take a while and use a lot of memory while doing so."; "_advanced_" = "Advanced"; "_permissions_" = "Permissions"; "_custom_permissions_" = "Custom permissions"; "_disable_files_app_" = "Disable Files App integration"; "_disable_files_app_footer_" = "Do not permit the access of files via the iOS Files application."; +"_trial_" = "Trial"; +"_trial_expired_day_" = "Days remaining"; "_time_remaining_" = "%@ remaining"; +"_disableLocalCacheAfterUpload_footer_" = "After uploading the file, do not keep it in the local cache"; +"_disableLocalCacheAfterUpload_" = "Disable local cache"; "_autoupload_" = "Auto upload photos/videos"; "_autoupload_select_folder_" = "Select the \"Auto upload\" folder"; +"_autoupload_error_select_folder_" = "Select a valid folder for the \"Auto upload\""; +"_autoupload_background_" = "Auto upload in the background"; "_autoupload_photos_" = "Auto upload photos"; "_autoupload_videos_" = "Auto upload videos"; "_autoupload_favorites_" = "Auto upload favorites only"; -"_autoupload_description_" = "Choose whether new photos or videos will be automatically uploaded to your account."; +//"_autoupload_description_" = "New photos/videos will be automatically uploaded to your MagentaCLOUD"; +"_autoupload_description_" = "Choose whether new photos or videos will be automatically uploaded to your MagentaCLOUD."; +"_autoupload_description_background_" = "This option requires the use of GPS to trigger the detection of new photos/videos in the camera roll once the location changes significantly"; +"_autoupload_background_title_" = "Limitations"; +"_autoupload_background_msg_" = "Due to iOS restrictions, it is not yet possible to perform background processes, unless GPS services are activated. Once the cell in the cellular network is changed, the system wakes up for a short time and checks for new photos to upload to the cloud."; +"_autoupload_change_location_" = "Change folder"; +"_autoupload_location_now_" = "Folder"; +"_autoupload_location_default_" = "Restore default folder"; +"_autoupload_change_location_footer_" = "Change folder used for \"Automatic upload of camera photos\" (if the option is enabled)"; +"_autoupload_not_select_home_" = "Select a folder"; +"_autoupload_save_album_" = "Copy photo or video into the photo album"; "_autoupload_fullphotos_" = "Upload the whole camera roll"; "_start_autoupload_" = "Turn on auto upload"; "_stop_autoupload_" = "Turn off auto upload"; @@ -273,48 +491,84 @@ "_autoupload_subfolder_granularity_" = "Subfolder Granularity"; "_filenamemask_" = "Change filename mask"; "_filenamemask_footer_" = "By default, when a file is uploaded, it automatically gets the following format: yy-mm-dd hh-mm-ss plus a 4-digit counter. If you do not like this format, you can change it here except for the final 4-digit counter, which cannot be omitted."; +"_autoupload_filenamemask_" = "Change filename mask"; +"_autoupload_filenamemask_footer_" = "Change the automatic filename mask"; "_autoupload_current_folder_" = "Currently selected folder"; +"_help_tutorial_" = "Tutorial"; +"_help_intro_" = "Introduction to Nextcloud"; +"_help_activity_verbose_" = "Detailed Activity feed"; +"_help_activity_mail_" = "Send activity via email"; +"_help_activity_clear_" = "Clear activity"; "_show_hidden_files_" = "Show hidden files"; "_format_compatibility_" = "Most Compatible"; "_format_compatibility_footer_" = "\"Most compatible\" will save photos as JPEG, if possible."; +"_terms_" = "Terms of Service"; "_privacy_" = "Privacy"; +"_privacy_policy_" = "Privacy Policy"; "_privacy_footer_" = "This app uses a service for the analysis of a crash. Your personal information is not sent with the report. If you want disable it, please change the setting \"Disable crash reporter\" to ON."; "_crashservice_title_" = "Disable crash reporter"; "_crashservice_alert_" = "This option requires a restart of the app to take effect."; "_upload_mov_livephoto_" = "Live Photo"; "_upload_mov_livephoto_footer_" = "\"Live Photo\" will save the selected photo in \"Live Photo\" format, if possible."; +"_view_capabilities_" = "View the capabilities"; "_capabilities_" = "Capabilities"; +"_no_capabilities_found_" = "Capabilities not found"; "_capabilities_footer_" = "Display the packages used by the app if they are installed and available on the server."; "_diagnostics_" = "Diagnostics"; "_diagnostics_footer_" = "Changing log level requires a restart of the app to take effect"; "_view_log_" = "View log file"; "_clear_log_" = "Clear log file"; +"_level_log_" = "Set Log level (disabled, standard, maximum)"; "_set_log_level_" = "Set Log level"; "_log_file_clear_alert_" = "Log file cleared \n successfully!"; "_connect_server_anyway_" = "Do you want to connect to the server anyway?"; "_server_is_trusted_" = "Do you consider this server trusted?"; "_connection_error_" = "Connection error"; +"_serverstatus_error_" = "Connection to server failure, verify your server address or network status"; +"_add_your_nextcloud_" = "Add your account"; "_login_url_" = "Server address https:// …"; +"_login_bottom_label_" = "Don't have a server yet?\nChoose one of the providers."; +"_error_multidomain_" = "Address not allowed, only the following domains are valid:"; +"_account_already_exists_" = "The account %@ already exists"; +"_traditional_login_" = "Revert to old login method"; +"_web_login_" = "Revert to web login method"; "_login_url_error_" = "URL error, please verify your server URL"; "_login_error_" = "Could not log in. Please try again later."; +"_webflow_not_available_" = "Web login not available, use the old login method"; "_favorites_" = "Favorites"; "_favorite_short_" = "Favorite"; +"_favorite_" = "Favorite"; +"_unfavorite_" = "Unfavorite"; +"_no_files_uploaded_" = "No files uploaded"; "_tutorial_favorite_view_" = "Files and folders you mark as favorites will show up here"; "_tutorial_offline_view_" = "Files copied here will be available offline.\n\nThey will be synchronized with your cloud."; "_tutorial_groupfolders_view_" = "No team folders yet"; +//"_tutorial_groupfolders_view_" = "No Group folders yet"; +"_tutorial_local_view_" = "You'll find the unpacked files from your cloud.\n\nConnect to iTunes to share these files."; "_more_" = "More"; "_favorite_no_files_" = "No favorites yet"; +"_pull_down_" = "Pull down to refresh"; +"_no_photo_load_" = "No photo or video"; +"_tutorial_autoupload_view_" = "You can enable auto uploads from \"Settings\""; "_no_date_information_" = "No date information"; "_no_camera_information_" = "No camera information"; "_no_lens_information_" = "No lens information"; "_today_" = "Today"; "_yesterday_" = "Yesterday"; +"_time_" = "Time: %@\n\n%@"; +"_location_not_enabled_" = "Location Services not enabled"; +"_location_not_enabled_msg_" = "Please go to \"Settings\" and turn on \"Location Services\""; "_access_photo_not_enabled_" = "Access to Photos is not enabled"; -"_access_photo_not_enabled_msg_" = "Please go to Settings → Apps → Nextcloud → Photos and enable Photo Library Access"; +//"_access_photo_not_enabled_msg_" = "Please go to \"Settings\" and turn on \"Photo Access\""; +"_access_photo_not_enabled_msg_" = "Please go to Settings → Apps → MagentaCLOUD → Photos and enable Photo Library Access"; "_access_background_app_refresh_denied_" = "\"Background App Refresh\" is denied. Please enable it in \"Settings\" otherwise, new photos or videos will not be detected when the application is in the background"; +"_access_photo_location_not_enabled_" = "Access to Photos and Location not enabled"; +"_access_photo_location_not_enabled_msg_" = "Please go to \"Settings\" and turn on \"Photo Access\" and \"Location Services\""; "_new_photos_starting_" = "Only photos or videos starting %@ will be uploaded."; "_tutorial_photo_view_" = "No photos or videos uploaded yet"; +"_create_full_upload_" = "Creating archive … May take a long time. During this process, keep the application active during the transfer as well."; "_error_createsubfolders_upload_" = "Error creating subfolders"; +"_activate_autoupload_" = "Enable auto upload"; "_remove_photo_CameraRoll_" = "Remove from camera roll"; "_remove_photo_CameraRoll_desc_" = "\"Remove from camera roll\" after uploads, a confirmation message will be displayed to delete the uploaded photos or videos from the camera roll. The deleted photos or videos will still be available in the iOS Photos Trash for 30 days."; "_never_" = "never"; @@ -325,25 +579,90 @@ "_hours_ago_" = "%d hours ago"; "_a_day_ago_" = "a day ago"; "_days_ago_" = "%d days ago"; +"_over_30_days_" = "over 30 days"; +"_connection_internet_offline_" = "The internet connection appears to be offline or Wi-Fi is required"; +"_insert_password_" = "Enter password"; +//"_update_in_progress_" = "Version upgrade, please wait …"; "_forbidden_characters_" = "Forbidden characters: %@"; "_cannot_send_mail_error_" = "No account is set up, or wrong email address entered."; "_open_url_error_" = "Cannot open the URL for this action."; +"_photo_camera_" = "Photos"; "_media_" = "Media"; +"_unzip_in_progress_" = "Extraction in progress on local storage …"; +"_file_unpacked_" = "File unpacked on local storage"; +"_file_saved_local_" = "File saved on local storage."; +"_file_not_present_" = "Error: File not present, please reload."; "_order_by_" = "Sort by"; "_name_" = "Name"; "_date_" = "Date"; "_size_" = "Size"; +"_order_by_name_a_z_" = "Sort by name (from A to Z)"; +"_sorted_by_name_a_z_" = "Sorted by name (from A to Z)"; +"_order_by_name_z_a_" = "Sort by name (from Z to A)"; +"_sorted_by_name_z_a_" = "Sorted by name (from Z to A)"; +"_order_by_date_more_recent_" = "Sort by newest"; +"_sorted_by_date_more_recent_" = "Sorted by newest"; +"_order_by_date_less_recent_" = "Sort by oldest"; +"_sorted_by_date_less_recent_" = "Sorted by oldest"; +"_order_by_size_smallest_" = "Sort by smallest"; +"_sorted_by_size_smallest_" = "Sorted by smallest"; +"_order_by_size_largest_" = "Sort by largest"; +"_sorted_by_size_largest_" = "Sorted by largest"; "_delete_selected_files_" = "Delete files"; "_remove_favorites_" = "Unfavorite"; "_add_favorites_" = "Favorite"; +"_move_selected_files_" = "Move files"; +"_move_or_copy_selected_files_" = "Move or copy files"; +"_download_selected_files_" = "Download files"; +"_download_selected_files_folders_" = "Download files and folders"; +"_error_operation_canc_" = "Error: Operation canceled."; +"_only_lock_passcode_" = "Available only with Lock password activated. Activate it in the \"Settings\"."; +"_go_to_app_settings_" = "Go to app settings"; +"_passcode_protection_" = "Password protection"; +"_remove_favorites_" = "Remove from favorites"; +"_remove_offline_" = "Remove from offline"; +"_add_favorites_" = "Add to favorites"; +"_add_offline_" = "Add to offline"; +"_remove_passcode_" = "Remove password protection"; +"_protect_passcode_" = "Protect with password"; "_share_" = "Share"; +"_reload_" = "Reload"; +"_open_in_" = "Open in …"; +"_open_" = "Open …"; "_remove_local_file_" = "Remove locally"; +"_add_local_" = "Add to local storage"; +"_comm_erro_pull_down_" = "Attention: Communication error with the server. Pull down to refresh."; +"_file_not_downloaded_" = "file not downloaded"; +"_file_not_uploaded_" = "file not uploaded"; "_folders_" = "folders"; "_folder_" = "folder"; "_files_" = "files"; "_file_" = "file"; +"_folder_blocked_" = "Folder blocked"; +"_downloading_progress_" = "Initiating download of files …"; "_no_file_pull_down_" = "Upload a file or pull down to refresh"; "_no_file_no_permission_to_create_" = "You don't have permission to create or upload files in this folder."; +"_browse_images_" = "Browse images"; +"_synchronized_folder_" = "Keep the folder synchronized"; +"_remove_synchronized_folder_" = "Turn off the synchronization"; +"_synchronized_confirm_" = "After enabling the synchronization, all files in the folder will be synchronized with the server, continue?"; +"_offline_folder_confirm_" = "After enabling the offline folder, all files in it will be synchronized with the server, continue?"; +"_file_not_found_reload_" = "File not found, pull down to refresh"; +"_title_section_download_" = "DOWNLOAD"; +"_download_" = "Download"; +"_title_section_upload_" = "UPLOAD"; +"_group_alphabetic_yes_" = "✓ Group alphabetically"; +"_group_alphabetic_no_" = "Group alphabetically"; +"_group_typefile_yes_" = "✓ Group by file type"; +"_group_typefile_no_" = "Group by file type"; +"_group_date_yes_" = "✓ Group by date"; +"_group_date_no_" = "Group by date"; +"_element_" = "element"; +"_elements_" = "elements"; +"_tite_footer_upload_wwan_" = " Wi-Fi network required, %lu %@ to upload"; +"_tite_footer_upload_" = "%lu %@ to upload"; +"_tite_footer_download_wwan_" = " Wi-Fi network required, %lu %@ to download"; +"_tite_footer_download_" = "%lu %@ to download"; "_limited_dimension_" = "Maximum size reached"; "_save_selected_files_" = "Save to photo gallery"; "_file_not_saved_cameraroll_" = "Error: File not saved in photo album"; @@ -352,24 +671,36 @@ "_directory_on_top_" = "Sort folders before files"; "_show_description_" = "Show folder description"; "_show_recommended_files_" = "Show recommendations"; +"_no_description_available_" = "No description available for this folder"; +"_folder_automatic_upload_" = "Folder for \"Auto upload\""; "_search_no_record_found_" = "No result"; "_search_in_progress_" = "Search in progress …"; "_search_instruction_" = "Search for file (minimum 2 characters)"; "_files_no_files_" = "No files in here"; "_files_no_folders_" = "No folders in here"; "_request_in_progress_" = "Request to server in progress …"; -"_personal_files_only_" = "Personal files only"; +"_personal_files_only_" = "Personal files only"; "audio" = "AUDIO"; +"compress" = "COMPRESS"; "directory" = "FOLDERS"; "document" = "DOCUMENTS"; "image" = "IMAGES"; "template" = "TEMPLATES"; +"unknow" = "UNKNOWN"; "video" = "VIDEO"; +"_file_del_only_local_" = "File not present on server"; +"_copy_file_" = "Copy"; "_paste_file_" = "Paste"; +"_open_quicklook_" = "Open with Quick Look"; +"_search_this_folder_" = "Search in this folder"; +"_search_all_folders_" = "Search in all folders"; +"_search_sub_folder_" = "Search here and in subfolders"; +"_theming_is_light_" = "Server theming too brightly coloured, not applicable"; "_cancel_all_task_" = "Cancel all transfers"; "_status_wait_download_" = "Waiting for download"; +"_status_in_download_" = "In download"; "_status_downloading_" = "Downloading"; "_status_wait_upload_" = "Waiting to upload"; "_status_wait_create_folder_" = "Waiting to create the folder"; @@ -378,12 +709,19 @@ "_status_wait_favorite_" = "Waiting to change favorite"; "_status_wait_copy_" = "Waiting to copy"; "_status_wait_move_" = "Waiting to move"; +"_status_in_upload_" = "In upload"; "_status_uploading_" = "Uploading"; "_status_upload_error_" = "Error, waiting to upload"; "_select_media_folder_" = "Set Media folder"; "_media_viewimage_show_" = "Show only images"; "_media_viewvideo_show_" = "Show only video"; "_media_show_all_" = "Show both"; +"_media_by_created_date_" = "Sort by created date"; +"_media_by_upload_date_" = "Sort by upload date"; +"_media_by_modified_date_" = "Sort by modified date"; +"_insert_password_pfd_" = "Secured PDF. Enter password"; +"_password_pdf_error_" = "Wrong password"; +"_error_download_photobrowser_" = "Error: Unable to download photo"; "_good_morning_" = "Good morning"; "_good_day_" = "Good day"; "_good_afternoon_" = "Good afternoon"; @@ -395,78 +733,197 @@ // MARK: Share "_share_link_" = "Share link"; +//"_share_link_" = "Share link"; +"_share_link_" = "Link"; +"_share_link_button_" = "Send link to …"; "_share_link_name_" = "Link name"; "_password_" = "Password"; "_share_password_" = "Password protected link"; +"_share_expirationdate_" = "Set expiration date for link"; "_date_" = "Date"; +"_share_title_" = "Share"; +"_add_sharee_" = "Add users or groups"; +"_add_sharee_footer_" = "You can share this resource by adding users or groups. To remove a share, remove all users and groups"; +"_find_sharee_title_" = "Search"; +"_find_sharee_" = "Search for user or group …"; +"_find_sharee_footer_" = "Enter part of the name of the user or group to search for (at least 2 characters) followed by \"Return\", select the users that should be allowed to access the share followed by \"Done\" to confirm"; +"_user_is_group_" = "(Group)"; +"_direct_sharee_title_" = "Share"; +"_direct_sharee_footer_" = "If you already know the name, enter it, then select the share type and press \"Done\" to confirm"; +"_direct_sharee_" = "Enter the username …"; "_user_sharee_footer_" = "Tap to change permissions"; +"_share_type_title_" = "Type of share"; +"_share_type_user_" = "User"; +"_share_type_group_" = "Group"; +"_share_type_remote_" = "Remote"; "_enforce_password_protection_" = "Enforce password protection"; +"_password_obligatory_" = "Enforce password protection enabled, password obligatory"; "_shared_with_you_by_" = "Shared with you by"; -"_shareLinksearch_placeholder_" = "Name, email, or Federated Cloud ID …"; +"_share_reshare_allowed_" = "Resharing is allowed."; +"_share_reshare_not_allowed_" = "Resharing is not allowed."; +"_sharing_message_" = "You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration."; +"_create_link_" = "Create Link"; +"personal_share_by_mail" = "Personal share by mail"; +"_your_shares_" = "Your Shares"; +"_share_linklabel_" = "Link '%@'"; +"_share_link_folder_" = "Link to folder"; +"_share_link_file_" = "Link to file"; +"no_shares_created" = "No shares created yet."; +"_advance_permissions_" = "Advanced permissions"; +"_send_new_email_" = "Send new email"; +"_apply_changes_" = "Apply changes"; +"_send_share_" = "Send share"; +"_PERMISSIONS_" = "PERMISSIONS"; +"share_editing_message" = "There are no editing functions for files unless you share them with MagentaCLOUD users."; +"_file_drop_message_" = "With File drop, only uploading is allowed. Only you can see files and folders that have been uploaded."; +"_custom_link_label" = "Your custom link label"; +"_set_password_" = "Set password"; +"_share_expiration_date_placeholder_"= "Expiration date for this share"; +"_share_download_limit_" = "Download Limit"; +"_share_download_limit_placeholder_" = "Enter download limit"; +"_share_download_limit_alert_empty_" = "Download limit cannot be empty."; +"_share_download_limit_alert_zero_" = "Download limit should be greater than 0."; +"_share_download_limit_alert_invalid_" = "Please enter a valid number for the download limit."; +"_share_remaining_download_" = "Downloads:"; +"_share_read_only_" = "View only"; +"_share_editing_" = "Can edit"; +"_share_file_drop_" = "Filedrop only"; +"_share_hide_download_" = "Prevent download"; +//"_share_note_recipient_" = "YOUR MESSAGE"; +"_share_note_recipient_" = "Note to recipient"; +"_shareLinksearch_placeholder_" = "Contact name or email address"; +"_shareLinksearch_mail_placeholder_" = "Type a name or an email and press Enter"; "_new_comment_" = "New comment …"; "_edit_comment_" = "Edit comment"; "_delete_comment_" = "Delete comment"; -"_share_read_only_" = "View only"; -"_share_editing_" = "Can edit"; +"_share_reshare_allowed_" = "Resharing is allowed."; +"_share_reshare_not_allowed_" = "Resharing is not allowed."; +"_sharing_message_" = "You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration."; +"_create_link_" = "Create Link"; +"personal_share_by_mail" = "Personal share by mail"; +"_your_shares_" = "Your Shares"; +"_share_linklabel_" = "Link '%@'"; +"_share_link_folder_" = "Link to folder"; +"_share_link_file_" = "Link to file"; +"no_shares_created" = "No shares created yet."; +"_advance_permissions_" = "Advanced permissions"; +"_send_new_email_" = "Send new email"; +"_apply_changes_" = "Apply changes"; +"_send_share_" = "Send share"; +"_PERMISSIONS_" = "PERMISSIONS"; +"share_editing_message" = "There are no editing functions for files unless you share them with MagentaCLOUD users."; +"_file_drop_message_" = "With File drop, only uploading is allowed. Only you can see files and folders that have been uploaded."; +"_custom_link_label" = "Your custom link label"; +"_set_password_" = "Set password"; +"_share_download_limit_" = "Download Limit"; +"_share_download_limit_placeholder_" = "Enter download limit"; +"_share_download_limit_alert_empty_" = "Download limit cannot be empty."; +"_share_download_limit_alert_zero_" = "Download limit should be greater than 0."; +"_share_allow_editing_" = "Allow editing"; "_share_allow_upload_" = "Allow upload and editing"; -"_share_file_drop_" = "File request"; "_share_secure_file_drop_" = "Secure file drop (upload only)"; -"_share_hide_download_" = "Hide download"; "_share_allowed_downloads_" = "Allowed downloads"; "_share_limit_download_" = "Limit downloads"; "_remaining_" = "%d remaining"; "_share_password_protect_" = "Password protection"; "_share_expiration_date_" = "Set expiration date"; -"_share_note_recipient_" = "Note to recipient"; +"_share_delete_sharelink_" = "Delete link"; "_share_add_sharelink_" = "Add another link"; "_share_can_read_" = "Read"; -"_share_can_reshare_" = "Share"; -"_share_can_create_" = "Create"; -"_share_can_change_" = "Edit"; -"_share_can_delete_" = "Delete"; -"_share_can_download_" = "Allow download and sync"; -"_share_unshare_" = "Delete share"; +"_share_can_reshare_" = "Allow resharing"; +"_share_can_create_" = "Allow creating"; +"_share_can_change_" = "Allow editing"; +"_share_can_delete_" = "Allow deleting"; +"_share_can_download_" = "Allow download"; +"_share_unshare_" = "Unshare"; "_share_internal_link_" = "Internal link"; +"_share_internal_link_des_" = "Only works for users with access to this file/folder"; +"_share_reshare_disabled_" = "You are not allowed to reshare this file/folder"; +"_share_reshare_restricted_" = "Note: You only have limited permission to reshare this file/folder"; +"_share_can_download_" = "Allow download"; "_share_internal_link_des_" = "Only works for users with access to this file/folder."; "_share_reshare_disabled_" = "You are not allowed to reshare this file/folder."; "_share_reshare_restricted_" = "Note: You only have limited permission to reshare this file/folder."; +"_create_new_link_" = "Create new link"; +"_share_send_link_by_mail_" = "Send link by mail"; +"_share_or_" = "or"; +"_share_copy_link_" = "Copy link"; +"_share_shared_with_" = "Shared with"; +"_share_quick_permission_everyone_can_just_upload_" = "Everyone can just upload"; +"_share_quick_permission_everyone_can_edit_" = "Everyone can edit"; +"_share_quick_permission_everyone_can_only_view_" = "Everyone can only view"; +"_share_quick_permission_everyone_can_just_upload_short_" = "Just upload"; +"_share_quick_permission_everyone_can_edit_short_" = "Can edit"; +"_share_quick_permission_everyone_can_only_view_short_" = "Only view"; +"_share_received_shares_text_" = "This file / folder was shared with you by"; +"_share_no_shares_text_" = "You have not yet shared your file/folder. Share to give others access"; +"_share_size_" = "Size: "; +"_share_modified_" = "Modified: "; +"_share_created_" = "Created: "; +"_share_uploaded_" = "Uploaded: "; +"_share_details_" = "Details"; +"_share_via_link_menu_password_label_" = "Password protect (%1$s)"; +"_share_link_empty_exp_date_" = "You must select expiration date."; +"_share_link_empty_note_message_" = "Please enter note."; "_remote_" = "Remote"; "_remote_group_" = "Remote group"; "_conversation_" = "Conversation"; +//"_share_can_reshare_" = "Allow resharing"; +//"_share_can_create_" = "Allow creating"; +//"_share_can_change_" = "Allow editing"; +//"_share_can_delete_" = "Allow deleting"; +//"_share_unshare_" = "Unshare"; +//"_share_can_download_" = "Allow download"; "_no_transfer_" = "No transfers yet"; "_no_transfer_sub_" = "Uploads and downloads from this device will show up here"; +"_no_activity_" = "No activity yet"; "_no_activity_footer_" = "No more activities to load"; "_transfers_" = "Transfers"; "_activity_" = "Activity"; +"_activity_file_not_present_" = "File no longer present"; "_trash_file_not_found_" = "It seems that the file is not in the Trash. Go to the Trash to update it and try again."; "_list_shares_" = "Shares"; "_list_shares_no_files_" = "No shares yet"; "_tutorial_list_shares_view_" = "Files and folders you share will show up here"; +"_create_synchronization_" = "Create synchronization"; "_offline_" = "Offline"; +"_local_storage_" = "Local storage"; +"_local_storage_no_record_" = "No files yet"; "_upload_photos_videos_" = "Upload photos or videos"; "_upload_file_" = "Upload file"; +"_upload_file_text_" = "Create text file"; "_create_nextcloudtext_document_" = "Create text document"; +"_save_document_picker_" = "Save here"; +"_destination_folder_" = "Destination folder"; "_use_folder_auto_upload_" = "Use the \"Auto upload\" folder as destination"; +"_rename_filename_" = "Rename"; "_filename_" = "Filename"; "_enter_filename_" = "Enter filename …"; "_default_preview_filename_footer_" = "Example preview of filename: IMG_0001.JPG"; "_filename_header_" = "Enter filename"; "_preview_filename_" = "Example preview of filename. You can use the mask %@ for date/time."; "_add_filenametype_" = "Specify type in filename"; +"_filenametype_photo_video_" = "Photo/Video"; "_maintain_original_filename_" = "Maintain original filename"; +"_modify_photo_" = "Modify photo"; "_notifications_" = "Notifications"; +"_no_notification_" = "No notifications yet"; +"_autoupload_filename_title_" = "Auto upload filename"; "_untitled_" = "Untitled"; +"_untitled_txt_" = "Untitled.txt"; +"_text_upload_title_" = "Upload text file"; "_e2e_settings_title_" = "Encryption"; "_e2e_settings_" = "End-to-end encryption"; "_e2e_settings_start_" = "Start end-to-end encryption"; +"_e2e_settings_not_available_" = "End-to-end encryption not available"; "_e2e_settings_activated_" = "End-to-end encryption activated"; "_e2e_server_disabled_" = "End-to-end encryption app disabled on server"; "_e2e_settings_view_passphrase_" = "All 12 words together make a very strong password, letting only you view and make use of your encrypted files. Please write it down and keep it somewhere safe."; "_e2e_settings_read_passphrase_" = "Read passphrase"; "_e2e_settings_lock_not_active_" = "Lock not active. Go to \"Settings\" and activate it."; "_e2e_settings_the_passphrase_is_" = "The passphrase is:"; -"_e2e_passphrase_request_title_" = "Request passphrase"; +"_e2e_passphrase_request_title_" = "Enter passphrase"; //"Request passphrase"; "_e2e_passphrase_request_message_" = "Insert the 12 words"; "_e2e_settings_remove_" = "Remove the encryption locally"; "_e2e_settings_remove_message_" = "Confirm removal of encryption along with the passphrase."; @@ -475,23 +932,79 @@ "_e2e_file_encrypted_" = "File encrypted"; "_e2e_goto_settings_for_enable_" = "This is an encrypted directory, go to \"Settings\" and enable end-to-end encryption"; "_e2e_in_upload_" = "Upload in progress, please wait for all files to be transferred."; +"_e2e_delete_folder_not_permitted_" = "Deletion of the directory marked as \"encrypted\" is not allowed"; +"_e2e_error_encode_metadata_" = "Serious internal error in encoding metadata"; +"_e2e_error_decode_metadata_" = "Serious internal error in decoding metadata"; +"_e2e_error_create_encrypted_" = "Could not create encrypted file"; +"_e2e_error_update_metadata_" = "Update metadata error"; +"_e2e_error_store_metadata_" = "Could not save metadata"; +"_e2e_error_send_metadata_" = "Could not send metadata"; +"_e2e_error_delete_metadata_" = "Could not delete metadata"; +"_e2e_error_get_metadata_" = "Could not fetch metadata"; +"_e2e_error_not_enabled_" = "Serious internal error. End-to-end encryption not enabled"; +"_e2e_error_record_not_found_" = "Serious internal error. Records not found"; +"_e2e_error_unlock_" = "Could not unlock folder"; +"_e2e_error_lock_" = "Could not lock folder"; +"_e2e_error_delete_mark_folder_" = "Decrypt marked folder"; +"_e2e_error_mark_folder_" = "Encrypt folder"; +"_e2e_error_directory_not_empty_" = "The directory is not empty"; +"_e2e_error_not_move_" = "It is not possible move files to encrypted directory"; +"_e2e_error_not_versionwriteable_" = "The E2EE version of the server is not compatible with this client"; +"_start_e2e_encryption_1_" = "To set up end-to-end encryption, they must first set up the PIN lock to prevent unauthorised people from accessing your key."; +"_start_e2e_encryption_2_" = "After starting the encryption, a randomly generated word sequence (passphrase) of 12 words is displayed. This remains in this app and can be displayed again. Nevertheless, we recommend that you write down the passphrase."; +"_start_e2e_encryption_3_" = "The passphrase is your personal password with which you can access encrypted data in your MagentaCLOUD or enable access to these files on other devices such as your PC."; +"_read_passphrase_description_" = "Here you can display the passphrase again and also copy it. You need the passphrase if you want to decrypt the data on another device with access to MagentaCLOUD, for example your PC or another smartphone or tablet."; +"_remove_passphrase_desc_1_" = "You can remove the passphrase on this device. This will not affect the encrypted content, but this device will no longer be able to decrypt your data."; +"_remove_passphrase_desc_2_" = "You can re-enter the passphrase here at any time to ensure access to your encrypted content from this device."; +"_e2e_error_incorrect_passphrase_" = "Wrong password?"; +"_e2e_error_passphrase_title" = "Error while decrypting."; "_scans_document_" = "Scan document"; -"_scanned_images_" = "Scanned images"; +//"_scanned_images_" = "Scanned images"; +"_scanned_images_" = "Scanned documents"; "_scan_document_pdf_page_" = "Page"; "_scan_label_document_zone_" = "Tap or drag images down for document creation"; "_filter_document_" = "Document"; "_filter_original_" = "Original"; +"_filter_bn_" = "Black & White"; +"_filter_grayscale_" = "Grayscale"; "_quality_image_title_" = "Preview image quality"; +"_quality_high_" = "Large file size of high quality"; +"_quality_medium_" = "Average file size of medium quality"; +"_quality_low_" = "Small file size of low quality"; +"_file_type_" = "File type"; +"_pdf_password_" = "PDF Password"; "_file_creation_" = "File creation"; "_delete_all_scanned_images_" = "Delete all scanned images"; "_text_recognition_" = "Text recognition"; "_all_files_" = "All files"; "_personal_files_" = "Personal Files"; +/* The title on the navigation bar of the Scanning screen. */ +"wescan.scanning.title" = "Scanning"; +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "Next"; +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "Edit Scan"; +/* The "Done" button on the right side of the navigation bar on the Review screen. */ +"wescan.review.button.done" = "Done"; +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "Review"; + "_trash_view_" = "Deleted files"; +"_trash_restore_all_" = "Restore all files"; +"_trash_delete_all_" = "Empty trash"; +"_trash_delete_permanently_" = "Delete permanently"; +"_trash_delete_all_description_" = "Do you want to empty the trash bin?";"_trash_no_trash_" = "No files deleted"; "_empty_trash_" = "Empty trash"; +"_trash_restore_all_" = "Restore all files"; +"_trash_delete_all_" = "Empty trash"; +"_trash_delete_permanently_" = "Delete permanently"; +"_trash_delete_all_description_" = "Do you want to empty the trash bin?"; "_trash_no_trash_" = "No files deleted"; "_trash_no_trash_description_" = "You can restore deleted files from here"; +"_trash_restore_selected_" = "Restore selected files"; +"_trash_delete_selected_" = "Delete selected files"; +"_recover_" = "Recover"; "_confirm_delete_selected_" = "Are you sure you want to delete the selected items?"; "_manage_file_offline_" = "Manage offline files"; "_set_available_offline_" = "Set as available offline"; @@ -507,7 +1020,62 @@ "_log_in_" = "Log in"; "_sign_up_" = "Sign up with provider"; "_host_your_own_server" = "Host your own server"; +"_unauthorized_" = "Unauthorized"; +"_bad_username_password_" = "Wrong username or password"; +"_cancelled_by_user" = "Transfer canceled"; +"_error_folder_destiny_is_the_same_" = "It is not possible to move the folder into itself"; +"_error_not_permission_" = "You don't have permission to complete the operation"; +"_error_path_" = "Unable to open this file or folder. Please make sure it exists"; +"_file_upload_not_exitst_" = "The file that you want to upload does not exist"; +"_forbidden_characters_from_server_" = "The name contains at least one invalid character"; +"_error_not_modified_" = "Resource not modified"; +"_not_connected_internet_" = "Server connection error"; +"_not_possible_connect_to_server_" = "It is not possible to connect to the server at this time"; +"_not_possible_create_folder_" = "Folder could not be created"; +"_server_down_" = "Could not establish contact with server"; +"_time_out_" = "Timeout, try again"; +"_unknow_response_server_" = "Unexpected response from server"; +"_user_authentication_required_" = "User authentication required"; +"_file_directory_locked_" = "File or directory locked"; +"_ssl_certificate_untrusted_" = "The certificate for this server is invalid"; +"_ssl_certificate_changed_" = "The certificate for this server seems to have changed"; +"_internal_server_" = "Internal server error"; +"_file_already_exists_" = "Could not complete the operation, a file with the same name exists"; +"_file_folder_not_exists_" = "The source file wasn't found at the specified path"; +"_folder_contents_nochanged_" = "The folder contents have not changed"; +"_images_invalid_converted_" = "The image is invalid and cannot be converted to a thumbnail"; +"_method_not_expected_" = "Unexpected request method"; +"_reauthenticate_user_" = "Access expired, log in again"; +"_server_error_retry_" = "The server is temporarily unavailable"; +"_too_many_files_" = "Too many files would be involved in this operation"; +"_too_many_request_" = "Sending too many requests caused the rate limit to be reached"; +"_user_over_quota_" = "Storage quota is reached"; +"_ssl_connection_error_" = "Connection SSL error, try again"; +"_bad_request_" = "Bad request"; +"_webdav_locked_" = "WebDAV Locked: Trying to access locked resource"; +"_error_user_not_available_" = "The user is no longer available"; +"_server_response_error_" = "Server response content error"; +"_no_nextcloud_found_" = "Server not found"; +"_error_decompressing_" = "Error during decompressing. Unknown compression method or the file is corrupt"; +"_error_json_decoding_" = "Serious internal error in decoding metadata (The data couldn't be read because it isn't in the correct format.)"; +"_error_check_remote_user_" = "Server responded with an error. Please log in again"; +"_request_entity_too_large_" = "The file is too large"; +"_not_possible_download_" = "It is not possible to download the file"; +"_not_possible_upload_" = "It is not possible to upload the file"; +"_error_files_upload_" = "Error uploading files"; +"_method_not_allowed_" = "The requested method is not supported"; "_invalid_url_" = "Invalid server URL"; +"_invalid_literal_" = "Invalid search string"; +"_invalid_date_format_" = "Invalid date format"; +"_invalid_data_format_" = "Invalid data format"; +"_error_decode_xml_" = "Invalid response, error decode XML"; +"_internal_generic_error_" = "internal error"; +"_editor_unknown_" = "Failed to open file: Editor is unknown"; +"_err_file_not_found_" = "File not found, removed"; +"_err_e2ee_app_version_" = "The app version of end-to-end encryption is not compatible with the server, please update your server"; +"_err_permission_microphone_" = "Please allow Microphone usage from Settings"; +"_err_permission_photolibrary_" = "Please allow Photos from Settings"; +"_err_permission_locationmanager_" = "Please allow Location - Always from Settings"; "_ssl_certificate_untrusted_" = "The certificate for this server is invalid."; "_ssl_certificate_changed_" = "The certificate for this server seems to have changed."; "_file_already_exists_" = "Could not complete the operation, a file with the same name exists."; @@ -515,11 +1083,20 @@ "_internal_generic_error_" = "internal error."; "_err_permission_microphone_" = "Please allow Microphone usage from Settings."; "_err_permission_photolibrary_" = "Please allow Photos from Settings."; +//"_ssl_certificate_untrusted_" = "The certificate for this server is invalid."; +//"_ssl_certificate_changed_" = "The certificate for this server seems to have changed."; +//"_file_already_exists_" = "Could not complete the operation, a file with the same name exists."; +//"_error_files_upload_" = "Error uploading files."; +//"_internal_generic_error_" = "internal error."; +//"_err_permission_microphone_" = "Please allow Microphone usage from Settings."; +//"_err_permission_photolibrary_" = "Please allow Photos from Settings."; "_qrcode_not_authorized_" = "This app is not authorized to use the Back Camera."; "_qrcode_not_supported_" = "QR code not supported by the current device."; "_create_voice_memo_" = "Create voice memo"; "_voice_memo_start_" = "Tap to start"; "_voice_memo_stop_" = "Tap to stop"; +"_voice_memo_filename_" = "Voice memo"; +"_voice_memo_title_" = "Upload voice memo"; "Enter Passcode" = "Enter Passcode"; "Enter a new passcode" = "Enter a new passcode"; "Confirm new passcode" = "Confirm new passcode"; @@ -547,10 +1124,15 @@ "_3_months_" = "3 months"; "_1_month_" = "1 month"; "_1_week_" = "1 week"; +"_1_day_" = "1 day"; "_monthly_" = "Monthly"; "_yearly_" = "Yearly"; +"_weekly_" = "Weekly"; "_daily_" = "Daily"; -"_used_space_" = "Used space:"; +"_day_" = "Day"; +"_used_space_" = "Used space"; +"_open_in_onlyoffice_" = "Open in ONLYOFFICE"; +"_open_in_collabora_" = "Open with Collabora Online"; "_login_address_detail_" = "The link to your %@ web interface when you open it in the browser."; "_go_to_page_" = "Go to page"; "_page_" = "Page"; @@ -558,17 +1140,28 @@ "_invalid_page_" = "Invalid Page"; "_the_entered_page_number_does_not_exist_" = "The entered page number does not exist."; "_error_something_wrong_" = "Something went wrong"; +"_resolution_" = "Resolution"; "_try_download_full_resolution_" = "Download full resolution image"; "_full_resolution_image_info_" = "This may reveal more information about the photo."; "_download_audio_" = "Download the audio locally"; "_copied_path_" = "Copied path"; "_privacy_screen_" = "Hide content when app is inactive"; "_privacy_screen_footer_" = "Hide the content in the app and show a splash screen when the app is inactive."; +"_copy_path_" = "Copy path"; +"_certificates_" = "Certificates"; +//"_privacy_screen_" = "Splash screen when app inactive"; +"_saving_" = "Saving …"; +"_video_not_streamed_" = "The server does not allow video streaming, do you want to download it?"; +"_video_not_streamed_e2ee_" = "The server does not allow video streaming because it is encrypted, do you want to download it?"; +"_scan_" = "Scan"; "_in_" = "in"; "_enter_passphrase_" = "Enter passphrase (12 words)"; "_show_more_results_" = "⋯ Load more results"; "_waiting_for_" = "Waiting for:"; +"_waiting_" = "Waiting …"; "_reachable_wifi_" = "network reachable via Wi-Fi or cable"; +"_ITMS-90076_" = "Due to a change in the Nextcloud application identifier, the settings and password for accessing your cloud are reset, so please re-enter your account data and check your Settings. We are sorry about that."; +"_password_not_present_" = "Please re-insert your credentials."; "_copy_passphrase_" = "Copy passphrase"; "_ok_copy_passphrase_" = "OK and copy passphrase"; "_select_color_" = "Select the color"; @@ -576,10 +1169,17 @@ "_description_dashboardwidget_" = "Having the Dashboard always at your fingertips has never been easier."; "_description_fileswidget_" = "View your recent files and use the toolbar to speed up your operations."; "_description_toolbarwidget_" = "A toolbar to speed up your operations."; +"_no_data_available_" = "No data available"; +"_widget_available_nc25_" = "Widget only available starting with server version 25"; +"_keep_running_" = "Keep the app running for a better user experience"; +"_recent_activity_" = "Recent activity"; "_title_lockscreenwidget_" = "Status"; "_description_lockscreenwidget_" = "Keep an eye on available space and recent activity"; - +"_no_items_" = "No items"; +"_check_back_later_" = "Check back later"; +"_exporting_video_" = "Exporting video … Tap to cancel."; "_keep_running_" = "Keep the app running for a better user experience."; +//"_keep_running_" = "Keep the app running for a better user experience."; "_apps_nextcloud_detect_" = "Detected %@ apps"; "_add_existing_account_" = "Other %@ Apps has been detected, do you want to add an existing account?"; "_status_in_progress_" = "Status reading in progress …"; @@ -594,16 +1194,36 @@ "_mobile_config_" = "Download the configuration profile"; "_calendar_contacts_footer_warning_" = "Configuration profile can only be downloaded if Safari is set as default browser."; "_calendar_contacts_footer_" = "After downloading the profile you can install it from Settings."; +"_preview_" = "Preview"; +"_crop_" = "Crop"; "_modify_image_desc_" = "Tap on a file to modify or rename."; "_message_disable_livephoto_" = "This image is a Live Photo, changing it will lose the Live Photo effect."; +//"_modify_image_desc_" = "Tap the image for modify"; +//"_message_disable_livephoto_" = "This image is a Live Photo, changing it will lose the Live effect"; "_enable_livephoto_" = "Enable Live Photo"; "_disable_livephoto_" = "Disable Live Photo"; "_undo_modify_" = "Undo modifying"; +"_unauthorizedFilesPasscode_" = "Files app cannot be used with an activated passcode"; +"_disableFilesApp_" = "Files app cannot be used because it is disabled"; +"_reset_application_done_" = "Reset application, done."; "_rename_already_exists_" = "A file with this name already exists."; "_group_folders_" = "Team folders"; +"_created_" = "Created"; +"_recipients_" = "Recipients"; +"_are_sure_" = "Are you sure?"; +"_creation_" = "Creation"; +"_modified_" = "Modified"; +//"_group_folders_" = "Group folders"; "_play_from_files_" = "Play movie from a file"; "_play_from_url_" = "Play movie from URL"; "_valid_video_url_" = "Insert a valid URL"; +"_deletion_progess_" = "Deletion in progress"; +"_copying_progess_" = "Copying in progress"; +"_moving_progess_" = "Moving in progress"; +"_chunk_enough_memory_" = "It seems there is not enough space to send the file"; +"_chunk_create_folder_" = "The file could not be sent, please check the server log"; +"_chunk_files_null_" = "The file for sending could not be created"; +"_chunk_file_null_" = "The file could not be sent"; "_chunk_move_" = "The sent file could not be reassembled, please check the server log."; "_download_image_" = "Download image"; "_download_video_" = "Download video"; @@ -612,6 +1232,9 @@ "_reset_wrong_passcode_desc_" = "Enabling this will remove all accounts and local data after %d failed passcode entry attempts."; "_deviceOwnerAuthentication_" = "The biometric sensor has been temporarily disabled due to multiple failed attempts. Enter the device passcode to re-enable the sensor."; "_virus_detect_" = "Virus detected. Upload cannot be completed!"; +"_zoom_" = "Zoom"; +"_zoom_in_" = "Zoom in"; +"_zoom_out_" = "Zoom out"; "_select_photos_" = "Select photos"; "_selected_photo_" = "selected photo"; "_selected_photos_" = "selected photos"; @@ -627,12 +1250,16 @@ "_account_settings_" = "Account settings"; "_users_" = "Users"; "_users_footer_" = "Allows you to select an account every time you open the app."; +//"_users_footer_" = "Every time the app is reactivated, the account will be requested."; +"_additional_view_options_" = "Additional view options"; "_while_charging_" = "While charging"; +"_downloading_" = "Downloading"; +"_additional_options_" = "Additional options"; "_keep_screen_awake_" = "Keep screen awake\nwhile transferring files"; "_error_not_found_" = "The requested resource could not be found"; "_error_conflict_" = "The request could not be completed due to a conflict with the current state of the resource"; "_error_precondition_" = "The server does not meet one of the preconditions of the requester"; -"_downloading_" = "Downloading"; + "_additional_options_" = "Additional options"; "_unauthorizedFilesPasscode_" = "Files app cannot be used with an activated passcode"; "_disableFilesApp_" = "Files app cannot be used because it is disabled"; @@ -643,12 +1270,32 @@ "_recent_activity_" = "Recent activity"; "_maintenance_mode_" = "The server is currently in maintenance mode, which may take a while."; "_account_disabled_" = "Account disabled"; + +//Video +"_select_trace_" = "Select the trace"; +"_video_processing_" = "Video processing"; +"_video_being_processed_" = "Video being processed …"; +"_downloading_" = "Downloading"; +"_download_error_" = "Download error"; +"_subtitle_" = "Subtitle"; +"_dts_to_ac3_" = "The DTS is not supported, it requires a conversion to Dolby Digital"; +"_reuired_conversion_" = "This video takes a long time to convert."; +"_stay_app_foreground_" = "Keep the app in the foreground …"; +"_conversion_available_" = "The conversion is always available on menu"; +"_video_format_not_recognized_" = "This video needs to be processed to be played, do you want to do it now?"; +"_video_must_download_" = "This video needs to be downloaded and processed to be played, do you want to do it now?"; +"_conversion_max_compatibility_" = "Max compatibility, the conversion can take much longer"; +"_video_tap_for_close_" = "A slight pressure to close the processing"; +"_subtitle_not_found_" = "Subtitle not found"; +"_disable_" = "Disable"; +"_subtitle_not_dowloaded_" = "There are subtitles not downloaded locally"; "_user_" = "User"; "_add_subtitle_" = "Add an external subtitle"; "_add_audio_" = "Add an external audio"; "_upload_foreground_msg_" = "Do not close %@ to complete the transfer …"; "_upload_background_msg_" = "Files upload in progress …"; "_create_folder_error_" = "An error has occurred while creating the folder:\n%@.\n\nPlease resolve the issue as soon as possible.\n\nAll uploads are suspended until the problem is resolved.\n"; +"_rename_file_error_" = "An error has occurred while renaming the file:\n%@."; "_creating_dir_progress_" = "Creating directories in progress … keep the application active."; "_creating_db_photo_progress_" = "Creating photo archive in progress … keep the application active."; "_account_unauthorized_" = "There was an issue authorizing the account %@. Please log in again."; @@ -662,7 +1309,7 @@ "_recommended_files_" = "Recommended Files"; "_propagate_layout_" = "Apply the following change to all subfolders"; "_version_mismatch_error_" = "Version change: please open the main app to update the data."; -"_check_auto_upload_" = "Automatic photo upload is currently disabled.\nWith Nextcloud iOS you can automatically back up your photos and keep them safe on your server.\nJust open Settings → Auto Upload, choose the options you prefer, and turn it on with the \"%@\" button."; +"_check_auto_upload_" = "Automatic photo upload is currently disabled.\nWith MagentaCLOUD iOS you can automatically back up your photos and keep them safe on your server.\nJust open Settings → Auto Upload, choose the options you prefer, and turn it on with the \"%@\" button."; "_cancel_all_request_" = "Are you sure you want to cancel all transfers?"; "_cancel_all_" = "Cancel All"; "_dismiss_" = "Dismiss"; @@ -691,6 +1338,7 @@ "_tip_open_mediadetail_" = "Swipe up to show the details"; "_tip_autoupload_button_" = "Configure the options as you prefer (folder, Wi-Fi, new content, subfolders) and tap ‘Turn on auto uploading’. You can stop it at any time, adjust the settings, and enable it again."; +"_tip_autoupload_" = "You can now Auto Upload specific Albums from your Photos. You can enable Auto Upload here"; // MARK: Accessibility @@ -703,15 +1351,42 @@ You can stop it at any time, adjust the settings, and enable it again."; "_on_" = "On"; // a11y: On/Off "_off_" = "Off"; +"_grid_view_" = "Show grid view"; +"_list_view_" = "Show list view"; "_list_" = "List"; "_icons_" = "Icons"; +// MARK: Plan customer +"_leave_plan_title" = "We're sorry to see you go"; +"_leave_plan_description" = "You'll no longer have access to:"; +"_current_plan_" = "Current Plan"; +"_billing_plan_" = "Billing Plan"; +"_keep_plan_" = "Keep Plan"; +"_leave_plan_" = "Leave Plan"; +"_change_plan_" = "Change Plan"; +"_manage_plan_" = "Manage Plan"; +"_purchase_plan_" = "Purchase Plan"; +"_restore_plan_" = "Restore Purchased Plan"; +"_purchase_plan_description_" = "Purchases have been restored"; +"_choose_plan_" = "You should choose a plan in order to purchase it."; +"_already_plan_" = "The selected plan has already been bought."; +"_change_billing_" = "Change Billing"; +"_payment_method_" = "Payment Method"; + +// MARK: Mantis library +"Mantis.Done" = "Done"; +"Mantis.Cancel" = "Cancel"; +"Mantis.Reset" = "Reset"; +"Mantis.Original" = "Original"; +"Mantis.Square" = "Square"; + // MARK: Assistant "_assistant_task_unknown_" = "Unknown"; "_assistant_task_scheduled_" = "Scheduled"; "_assistant_task_in_progress_" = "In Progress"; "_assistant_task_completed_" = "Completed"; "_assistant_task_failed_" = "Failed"; +"_all_" = "All"; "_input_" = "Input"; "_output_" = "Output"; "_task_details_" = "Task details"; @@ -767,6 +1442,55 @@ You can stop it at any time, adjust the settings, and enable it again."; "_auto_upload_all_photos_warning_message_" = "This can take some time to process depending on the amount of photos."; "_item_with_same_name_already_exists_" = "An item with the same name already exists."; +// MARK: Privacy Policy +"_privacy_settings_title_" = "Privacy Settings"; +"_privacy_help_text_after_login_" = "This app uses Cookies and similiar technolgies (tools). By clicking Accept, you accept the processing and also the Transfer of your data to third parties. The data will be used for Analysis, retargeting and to Display personalized Content and Advertising on sites and third-party sites. You can find further informatin, including Information on data processing by third-party Providers, in the settings and in our Privacy Policy.You can reject the use of the Tools or customize them at any time in the Settings."; +"_key_privacy_help_" = "Privacy Policy"; +"_key_reject_help_" = "reject"; +"_key_settings_help_" = "Settings"; +"_accept_button_title_" = "Accept"; +"_privacy_settings_help_text_" = "To optimize your app, we collect anonymous data. For this we use software solutions of different partners. We would like to give you full transparency and decision-making power over the processin and collection of your anonymized usage data. You can also change your settings at any time later in the app settings under data protection. Please note, however, that data collection makes a considerable contribution to the optimization of this app and you prevent this optimization by preventing data transmission."; +"_required_data_collection_" = "Required data collection"; +"_required_data_collection_help_text_" = "The collection of this data is necessary to be able to use essential functions of the app."; +"_analysis_data_acqusition_" = "Analysis-data acqusition for the design"; +"_analysis_data_acqusition_help_text_" = "This data helps us to optimize the app usage for you and to identify system crashes and errors more quickly."; + +// MARK: Collabora +"_prefix_upload_path_" = "MagentaCLOUD/"; +"_please_enter_file_name_" = "Please enter the file name"; + +// MARK: Scan +"_location_" = "Location"; +"_prefix_upload_path_" = "MagentaCLOUD/"; + +"_save_with_text_recognition_" = "SAVE WITH TEXT RECOGNITION (OCR)"; +"_pdf_with_ocr_" = "PDF (OCR)"; +"_text_file_ocr_" = "Textfile (txt)"; +"_save_without_text_recognition_" = "SAVE WITHOUT TEXT RECOGNITION"; +"_pdf_" = "PDF"; +"_jpg_" = "JPG"; +"_png_" = "PNG"; +"_set_password_" = "Set password"; +"_no_password_warn_" = "Please enter a password for the PDF you want to create or disable the function."; +"_saved_info_alert_" = "Saving will take some time, especially if you have selected several pages and file formats."; +"_no_file_type_selection_error_" = "Please select at least one filetype"; +"_no_internet_alert_message_" = "A data connection is not currently allowed."; +"_no_internet_alert_title_" = "Connection error"; +"_auto_upload_help_text_" = "With this option, you upload your photos or videos to the same folder that you selected for \"Automatic upload.\""; +"_item_with_same_name_already_exists_" = "An item with the same name already exists."; + +// MARK: Dashboard +"_shared_" = "Shared"; +"_recieved_" = "Received"; + +// MARK: App Updater +"update_available" = "Update available"; +"update_description" = "MagentaCLOUD version %@ is now available"; +"update" = "Update"; +"not_now" = "Not Now"; + +"_prompt_insert_file_name" = "Please enter filename"; + // MARK: Migration Multi Domains "_preparing_migration_" = "Preparing migration …"; "_scanning_files_" = "Scanning files …"; From 4a6828406af2805f015631d8afddfc7c1bf420bc Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Thu, 9 Apr 2026 18:59:22 +0530 Subject: [PATCH 12/40] NMC 1984 - Privacy policy customisation updated changes with 10.3.0 version --- .../Data/NCManageDatabase+LocalFile.swift | 40 +- .../Data/NCManageDatabase+Metadata.swift | 81 +-- .../NCCollectionViewCommon+SyncMetadata.swift | 14 +- .../NCCollectionViewCommon.swift | 673 ++++++------------ .../NCBackgroundLocationUploadManager.swift | 20 +- iOSClient/NCGlobal.swift | 24 +- iOSClient/Networking/NCConfigServer.swift | 25 +- .../Networking/NCNetworking+Download.swift | 62 +- .../Networking/NCNetworking+LivePhoto.swift | 25 +- iOSClient/Networking/NCNetworking+Task.swift | 25 +- .../Networking/NCNetworking+Upload.swift | 27 +- .../Networking/NCNetworking+WebDAV.swift | 114 ++- iOSClient/Networking/NCNetworking.swift | 199 +----- .../Networking/NCNetworkingProcess.swift | 1 + iOSClient/Networking/NCService.swift | 25 +- .../PrivacyPolicyViewController.swift | 4 + .../PrivacySettingsViewController.swift | 9 +- .../en.lproj/Localizable.strings | 114 ++- 18 files changed, 470 insertions(+), 1012 deletions(-) diff --git a/iOSClient/Data/NCManageDatabase+LocalFile.swift b/iOSClient/Data/NCManageDatabase+LocalFile.swift index 334e5fba25..2146dec1ad 100644 --- a/iOSClient/Data/NCManageDatabase+LocalFile.swift +++ b/iOSClient/Data/NCManageDatabase+LocalFile.swift @@ -29,17 +29,14 @@ extension NCManageDatabase { // MARK: - Realm Write + /// Adds or updates multiple local file entries corresponding to the given metadata array. + /// Uses async Realm read + single write transaction. Assumes `tableLocalFile` has a primary key. /// - Parameters: - /// - metadata: The `tableMetadata` containing file details. - /// - offline: Optional flag to mark the file as available offline. - /// - Returns: Nothing. Realm write is performed asynchronously. - func addLocalFileAsync(metadata: tableMetadata, offline: Bool? = nil) async { - // Read (non-blocking): safely detach from Realm thread - let existing: tableLocalFile? = performRealmRead { realm in - realm.objects(tableLocalFile.self) - .filter(NSPredicate(format: "ocId == %@", metadata.ocId)) - .first - .map { tableLocalFile(value: $0) } + /// - metadatas: Array of `tableMetadata` to map into `tableLocalFile`. + /// - offline: Optional override for the `offline` flag applied to all items. + func addLocalFilesAsync(metadatas: [tableMetadata], offline: Bool? = nil) async { + guard !metadatas.isEmpty else { + return } // Extract ocIds for efficient lookup @@ -62,19 +59,20 @@ extension NCManageDatabase { // Reuse existing object or create a new one let local = existingMap[metadata.ocId] ?? tableLocalFile() - addObject.account = metadata.account - addObject.etag = metadata.etag - addObject.exifDate = NSDate() - addObject.exifLatitude = "-1" - addObject.exifLongitude = "-1" - addObject.ocId = metadata.ocId - addObject.fileName = metadata.fileName + local.account = metadata.account + local.etag = metadata.etag + local.exifDate = NSDate() + local.exifLatitude = "-1" + local.exifLongitude = "-1" + local.ocId = metadata.ocId + local.fileName = metadata.fileName - if let offline { - addObject.offline = offline - } + if let offline { + local.offline = offline + } - realm.add(addObject, update: .all) + realm.add(local, update: .all) + } } } diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index 9f06da7d92..67ed7ad2f2 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -151,37 +151,10 @@ extension tableMetadata { (fileNameView as NSString).deletingPathExtension } - var isRenameable: Bool { - if !NCMetadataPermissions.canRename(self) { - return false - } - if lock { - return false - } - if !isDirectoryE2EE && e2eEncrypted { - return false - } - return true - } - - var isPrintable: Bool { - if isDocumentViewableOnly { - return false - } - if ["application/pdf", "com.adobe.pdf"].contains(contentType) || contentType.hasPrefix("text/") || classFile == NKTypeClassFile.image.rawValue { - return true - } - return false - } - var isSavebleInCameraRoll: Bool { return (classFile == NKTypeClassFile.image.rawValue && contentType != "image/svg+xml") || classFile == NKTypeClassFile.video.rawValue } - var isDocumentViewableOnly: Bool { - sharePermissionsCollaborationServices == NCPermissions().permissionReadShare && classFile == NKTypeClassFile.document.rawValue - } - var isAudioOrVideo: Bool { return classFile == NKTypeClassFile.audio.rawValue || classFile == NKTypeClassFile.video.rawValue } @@ -262,7 +235,7 @@ extension tableMetadata { if !capabilities.fileSharingApiEnabled || (capabilities.e2EEEnabled && isDirectoryE2EE) { return false } - return true + return !e2eEncrypted } var hasPreviewBorder: Bool { @@ -371,17 +344,6 @@ extension tableMetadata { return !lock || (lockOwner == user && lockOwnerType == 0) } - // Return if is sharable - func isSharable() -> Bool { - guard let capabilities = NCNetworking.shared.capabilities[account] else { - return false - } - if !capabilities.fileSharingApiEnabled || (capabilities.e2EEEnabled && isDirectoryE2EE), !e2eEncrypted { - return false - } - return !e2eEncrypted - } - /// Returns a detached (unmanaged) deep copy of the current `tableMetadata` object. /// /// - Note: Primitive properties and lists of primitive values (for example `shareType`) @@ -548,6 +510,19 @@ extension NCManageDatabase { } } } + + func deleteMetadataOcIds(_ ocIds: [String]) { + do { + let realm = try Realm() + try realm.write { + let results = realm.objects(tableMetadata.self).filter("ocId IN %@", ocIds) + realm.delete(results) + } + } catch let error as NSError { + nkLog(error: "Could not access database: \(error)") + + } + } func replaceMetadataAsync(ocId: String, metadata: tableMetadata) async { let detached = metadata.detachedCopy() @@ -956,34 +931,6 @@ extension NCManageDatabase { .map { $0.detachedCopy() } } ?? [] } - - func getMediaMetadatas(predicate: NSPredicate, sorted: String? = nil, ascending: Bool = false) -> ThreadSafeArray? { - - do { - let realm = try Realm() - if let sorted { - var results: [tableMetadata] = [] - switch sorted {//NCPreferences().mediaSortDate { - case "date": - results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.date as Date) > ($1.date as Date) } - case "creationDate": - results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.creationDate as Date) > ($1.creationDate as Date) } - case "uploadDate": - results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.uploadDate as Date) > ($1.uploadDate as Date) } - default: - let results = realm.objects(tableMetadata.self).filter(predicate) - return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) - } - return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) - } else { - let results = realm.objects(tableMetadata.self).filter(predicate) - return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) - } - } catch let error as NSError { -// NextcloudKit.shared.nkCommonInstance.writeLog("Could not access database: \(error)") - } - return nil - } func getMetadatas(predicate: NSPredicate, sortedByKeyPath: String, diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift index 571306d2de..5e0254858d 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift @@ -21,7 +21,7 @@ extension NCCollectionViewCommon { // If a sync task is already running, do not start a new one if let task = syncMetadatasTask, !task.isCancelled { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .info, message: "Exit: Another sync is already running. Skipping this one.", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .info, message: "Exit: Another sync is already running. Skipping this one.", consoleOnly: true) return } @@ -45,9 +45,9 @@ extension NCCollectionViewCommon { func stopSyncMetadata() async { if let task = syncMetadatasTask { if task.isCancelled { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .stop, message: "Sync Metadata for \(self.serverUrl) was already cancelled.", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .stop, message: "Sync Metadata for \(self.serverUrl) was already cancelled.", consoleOnly: true) } else { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .stop, message: "Stopping active Sync Metadata for \(self.serverUrl).", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .stop, message: "Stopping active Sync Metadata for \(self.serverUrl).", consoleOnly: true) } } @@ -74,7 +74,7 @@ extension NCCollectionViewCommon { return } let identifier = self.serverUrl + "_syncMetadata" - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .start, message: "Start Sync Metadata for \(self.serverUrl)") + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .start, message: "Start Sync Metadata for \(self.serverUrl)") // Always cancel and clear all tracked URLSessionTask on any exit path defer { @@ -85,7 +85,7 @@ extension NCCollectionViewCommon { // If a readFile for this serverUrl is already in-flight, do nothing if await networking.networkingTasks.isReading(identifier: identifier) { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .debug, message: "ReadFile for this \(self.serverUrl) is already in-flight.", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .debug, message: "ReadFile for this \(self.serverUrl) is already in-flight.", consoleOnly: true) return } @@ -151,9 +151,9 @@ extension NCCollectionViewCommon { // If this folder failed, skip it but keep processing others if resultsReadFolder.error == .success { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .network, message: "Read correctly: \(serverUrl)", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .network, message: "Read correctly: \(serverUrl)", consoleOnly: true) } else { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .error, message: "Read failed for \(serverUrl) with error: \(resultsReadFolder.error.errorDescription)") + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .error, message: "Read failed for \(serverUrl) with error: \(resultsReadFolder.error.errorDescription)") return } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 2595fb1348..f8d9b7cefe 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -4,14 +4,12 @@ import UIKit import SwiftUI -import Realm import RealmSwift import NextcloudKit import EasyTipView import LucidBanner -import MoEngageInApps -class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionFirstHeaderDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { +class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { @IBOutlet weak var collectionView: UICollectionView! @@ -52,19 +50,14 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var tabBarSelect: NCCollectionViewCommonSelectTabBar? var attributesZoomIn: UIMenuElement.Attributes = [] var attributesZoomOut: UIMenuElement.Attributes = [] - var syncMetadatasTask: Task? - let maxImageGrid: CGFloat = 7 -// var headerMenu: NCSectionFirstHeader? - var tipViewAccounts: EasyTipView? - var selectableDataSource: [RealmSwiftObject] { dataSource.getMetadataSourceForAllSections() } + var syncMetadatasTask: Task? // DECLARE var layoutKey = "" var titleCurrentFolder = "" var titlePreviusFolder: String? var enableSearchBar: Bool = false - var groupByField = "name" var headerRichWorkspaceDisable: Bool = false var emptyImageName: String? @@ -158,7 +151,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return self.serverUrl == self.utilityFileSystem.getHomeServer(session: self.session) && capabilities.recommendations } - internal let debouncer = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerReloadDataSource = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerReloadData = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerGetServerData = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerNetworkSearch = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) // MARK: - View Life Cycle @@ -241,29 +237,28 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String UserDefaults.standard.set(appVersion, forKey: "CurrentAppVersion") } - + registerForTraitChanges([UITraitUserInterfaceStyle.self]) { [weak self] (view: NCCollectionViewCommon, _) in guard let self else { return } self.sectionFirstHeader?.setRichWorkspaceColor(style: view.traitCollection.userInterfaceStyle) } - NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterChangeTheming), object: nil, queue: .main) { [weak self] _ in - guard let self else { return } - self.collectionView.reloadData() + NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterChangeTheming), object: nil, queue: .main) { _ in + let serverUrl = self.serverUrl + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: serverUrl) + } + } } - - registerForTraitChanges([UITraitUserInterfaceStyle.self]) { [weak self] (view: NCCollectionViewCommon, _) in - guard let self else { return } - self.sectionFirstHeader?.setRichWorkspaceColor(style: view.traitCollection.userInterfaceStyle) + NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterUserInteractionMonitor), object: nil, queue: .main) { _ in + Task { + await self.debouncerReloadData.resume() + } } - NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterChangeTheming), object: nil, queue: .main) { [weak self] _ in - guard let self else { return } - self.collectionView.reloadData() - } - NotificationCenter.default.addObserver(self, selector: #selector(updateIcons), name: NSNotification.Name(rawValue: global.notificationCenterUpdateIcons), object: nil) DispatchQueue.main.async { @@ -274,6 +269,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) +// AnalyticsHelper.shared.displayInAppNotification() + if titlePreviusFolder != nil { navigationController?.navigationBar.topItem?.title = titlePreviusFolder } @@ -286,6 +283,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS isEditMode = false Task { + await NCNetworking.shared.transferDispatcher.addDelegate(self) + await (self.navigationController as? NCMainNavigationController)?.setNavigationLeftItems() await (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() } @@ -311,9 +310,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - Task { - await NCNetworking.shared.transferDispatcher.addDelegate(self) - } + // Re-evaluate in-app messages after viewDidAppear + MoEngageAnalytics.shared.displayInAppNotificationSafely(reason: "viewDidAppear") NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive(_:)), name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(closeRichWorkspaceWebView), name: NSNotification.Name(rawValue: global.notificationCenterCloseRichWorkspaceWebView), object: nil) @@ -370,7 +368,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } @objc func updateIcons() { - collectionView.reloadData() +// collectionView.reloadData() + Task { + await self.reloadDataSource() + } } func isApplicationUpdated() -> Bool { @@ -386,287 +387,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.present(newViewController!, animated: true, completion: nil) } - // MARK: - Transfer Delegate - - func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } - - func transferChange(status: String, metadatasError: [tableMetadata: NKError]) { - switch status { - // DELETE - case self.global.networkingStatusDelete: - let errorForThisServer = metadatasError.first { entry in - let (key, value) = entry - return key.serverUrl == self.serverUrl && value != .success - }?.value - - let needLoadDataSource = metadatasError.contains { entry in - let (key, value) = entry - return key.serverUrl == self.serverUrl && value == .success - } - - if let error = errorForThisServer { - NCContentPresenter().showError(error: error) - } - - if self.isSearchingMode { - self.networkSearch() - } else if needLoadDataSource { - Task { - await self.reloadDataSource() - } - } else { - Task.detached { - if await self.isRecommendationActived() { - await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) - } - } - } - default: - break - } - } - - func transferChange(status: String, metadata: tableMetadata, error: NKError) { - guard session.account == metadata.account else { return } - - if error != .success { - NCContentPresenter().showError(error: error) - } - - DispatchQueue.main.async { - switch status { - // UPLOADED, UPLOADED LIVEPHOTO - case self.global.networkingStatusUploaded, self.global.networkingStatusUploadedLivePhoto: - self.debouncer.call { - if self.isSearchingMode { - self.networkSearch() - } else if self.serverUrl == metadata.serverUrl { - Task { - await self.reloadDataSource() - } - } - } - // DOWNLOAD - case self.global.networkingStatusDownloading: - Task { - if metadata.serverUrl == self.serverUrl { - await self.reloadDataSource() - } - } - case self.global.networkingStatusDownloaded: - Task { - if metadata.serverUrl == self.serverUrl { - await self.reloadDataSource() - } - } - case self.global.networkingStatusDownloadCancel: - Task { - if metadata.serverUrl == self.serverUrl { - await self.reloadDataSource() - } - } - // CREATE FOLDER - case self.global.networkingStatusCreateFolder: - if metadata.serverUrl == self.serverUrl, metadata.sessionSelector != self.global.selectorUploadAutoUpload { - self.pushMetadata(metadata) - } - // RENAME - case self.global.networkingStatusRename: - self.debouncer.call { - if self.isSearchingMode { - self.networkSearch() - } else if self.serverUrl == metadata.serverUrl { - Task { - await self.reloadDataSource() - } - } - } - // FAVORITE - case self.global.networkingStatusFavorite: - self.debouncer.call { - if self.isSearchingMode { - self.networkSearch() - } else if self is NCFavorite { - Task { - await self.reloadDataSource() - } - } else if self.serverUrl == metadata.serverUrl { - Task { - await self.reloadDataSource() - } - } - } - default: - break - } - } - } - - func transferReloadData(serverUrl: String?, status: Int?) { - self.debouncer.call { - if self.isSearchingMode { - guard status != self.global.metadataStatusWaitDelete, - status != self.global.metadataStatusWaitRename, - status != self.global.metadataStatusWaitMove, - status != self.global.metadataStatusWaitCopy, - status != self.global.metadataStatusWaitFavorite else { - return - } - self.networkSearch() - } else if ( self.serverUrl == serverUrl) || serverUrl == nil { - Task { - await self.reloadDataSource() - } - } - } - } - - func transferRequestData(serverUrl: String?) { - self.debouncer.call { - if self.isSearchingMode { - self.networkSearch() - } else if ( self.serverUrl == serverUrl) || serverUrl == nil { - Task { - await self.getServerData() - } - } - } - } - - func transferCopy(metadata: tableMetadata, destination: String, error: NKError) { - if error != .success { - NCContentPresenter().showError(error: error) - } - - if isSearchingMode { - return networkSearch() - } - - if metadata.serverUrl == self.serverUrl || destination == self.serverUrl { - Task { - await self.reloadDataSource() - } - } - } - - func transferMove(metadata: tableMetadata, destination: String, error: NKError) { - if error != .success { - NCContentPresenter().showError(error: error) - } - - if isSearchingMode { - return networkSearch() - } - - if metadata.serverUrl == self.serverUrl || destination == self.serverUrl { - Task { - await self.reloadDataSource() - } - } - } - - func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } - - func transferChange(status: String, - account: String, - fileName: String, - serverUrl: String, - selector: String?, - ocId: String, - destination: String?, - error: NKError) { - Task { - if error != .success, - error.errorCode != global.errorResourceNotFound { - await showErrorBanner(controller: self.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode) - } - guard session.account == account else { - return - } - - await self.debouncer.call { - switch status { - // UPLOADED, UPLOADED LIVEPHOTO, DELETE - case self.global.networkingStatusUploaded, - self.global.networkingStatusDelete, - self.global.networkingStatusCopyMove: - if self.isSearchingMode { - self.networkSearch() - } else if self.serverUrl == serverUrl || destination == self.serverUrl { - await self.reloadDataSource() - } - // DOWNLOAD - case self.global.networkingStatusDownloaded: - if serverUrl == self.serverUrl || self.serverUrl.isEmpty { - await self.reloadDataSource() - } - case self.global.networkingStatusDownloadCancel: - if serverUrl == self.serverUrl { - await self.reloadDataSource() - } - // CREATE FOLDER - case self.global.networkingStatusCreateFolder: - if serverUrl == self.serverUrl, - selector != self.global.selectorUploadAutoUpload, - let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { - self.pushMetadata(metadata) - } - // RENAME - case self.global.networkingStatusRename: - if self.isSearchingMode { - self.networkSearch() - } else if self.serverUrl == serverUrl { - await self.reloadDataSource() - } - // FAVORITE - case self.global.networkingStatusFavorite: - if self.isSearchingMode { - self.networkSearch() - } else if self is NCFavorite { - await self.reloadDataSource() - } else if self.serverUrl == serverUrl { - await self.reloadDataSource() - } - default: - break - } - } - } - } - - func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) { - Task { - await self.debouncer.call { - if requestData { - if self.isSearchingMode { - self.networkSearch() - } else if ( self.serverUrl == serverUrl) || serverUrl == nil { - Task { - await self.getServerData() - } - } - } else { - if self.isSearchingMode { - guard status != self.global.metadataStatusWaitDelete, - status != self.global.metadataStatusWaitRename, - status != self.global.metadataStatusWaitMove, - status != self.global.metadataStatusWaitCopy, - status != self.global.metadataStatusWaitFavorite else { - return - } - self.networkSearch() - } else if ( self.serverUrl == serverUrl) || serverUrl == nil { - Task { - await self.reloadDataSource() - } - } - } - } - } - } - // MARK: - NotificationCenter @objc func applicationWillResignActive(_ notification: NSNotification) { @@ -743,12 +463,9 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return NCBrandOptions.shared.brand } - func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } - @MainActor func startGUIGetServerData() { self.dataSource.setGetServerData(false) - self.collectionView.reloadData() // Don't show spinner on iPad root folder if UIDevice.current.userInterfaceIdiom == .pad, @@ -778,61 +495,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.navigationItem.titleView = nil self.navigationItem.title = self.titleCurrentFolder } - - func resetPlusButtonAlpha(animated: Bool = true) { } - - func isHiddenPlusButton(_ isHidden: Bool) { } - - // MARK: - Empty - - func emptyDataSetView(_ view: NCEmptyView) { - - self.emptyDataSet?.setOffset(getHeaderHeight()) - if isSearchingMode { - view.emptyImage.image = UIImage(named: "search")?.image(color: .gray, size: UIScreen.main.bounds.width) - if self.dataSourceTask?.state == .running { - view.emptyTitle.text = NSLocalizedString("_search_in_progress_", comment: "") - } else { - view.emptyTitle.text = NSLocalizedString("_search_no_record_found_", comment: "") - } - view.emptyDescription.text = NSLocalizedString("_search_instruction_", comment: "") - } else if self.dataSourceTask?.state == .running { - view.emptyImage.image = UIImage(named: "networkInProgress")?.image(color: .gray, size: UIScreen.main.bounds.width) - view.emptyTitle.text = NSLocalizedString("_request_in_progress_", comment: "") - view.emptyDescription.text = "" - } else { - if serverUrl.isEmpty { - view.emptyImage.image = emptyImage - view.emptyTitle.text = NSLocalizedString(emptyTitle, comment: "") - view.emptyDescription.text = NSLocalizedString(emptyDescription, comment: "") - } else { - view.emptyImage.image = UIImage(named: "folder_nmcloud") - view.emptyTitle.text = NSLocalizedString("_files_no_files_", comment: "") - view.emptyDescription.text = NSLocalizedString("_no_file_pull_down_", comment: "") - } - } - - let spinner = UIActivityIndicatorView(style: .medium) - spinner.startAnimating() - - let container = UIView() - container.translatesAutoresizingMaskIntoConstraints = false - container.addSubview(spinner) - - spinner.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - spinner.centerXAnchor.constraint(equalTo: container.centerXAnchor), - spinner.centerYAnchor.constraint(equalTo: container.centerYAnchor) - ]) - - self.navigationItem.titleView = container - } - - @MainActor - func restoreDefaultTitle() { - self.navigationItem.titleView = nil - self.navigationItem.title = self.titleCurrentFolder - } // MARK: - SEARCH @@ -843,7 +505,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS searchController?.searchBar.alpha = 1 } else { searchController?.searchBar.alpha = 0.3 - } } @@ -874,6 +535,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.networking.cancelUnifiedSearchFiles() self.isSearchingMode = false + self.networkSearchInProgress = false self.literalSearch = "" self.providers?.removeAll() self.dataSource.removeAll() @@ -896,11 +558,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) { guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } - - NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) -// NCDownloadAction.shared.openShare(viewController: self, metadata: metadata, page: .sharing) TealiumHelper.shared.trackEvent(title: "magentacloud-app.filebrowser.sharing", data: ["": ""]) appDelegate.adjust.trackEvent(TriggerEvent(Sharing.rawValue)) + + NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) } func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { @@ -908,30 +569,30 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS toggleMenu(metadata: metadata, image: image, sender: sender) } - func tapRichWorkspace(_ sender: Any) { - if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { - if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { - viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" - viewerRichWorkspace.serverUrl = serverUrl - viewerRichWorkspace.delegate = self - - navigationController.modalPresentationStyle = .fullScreen - self.present(navigationController, animated: true, completion: nil) - } - } - } +// func tapRichWorkspace(_ sender: Any) { +// if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { +// if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { +// viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" +// viewerRichWorkspace.serverUrl = serverUrl +// viewerRichWorkspace.delegate = self +// +// navigationController.modalPresentationStyle = .fullScreen +// self.present(navigationController, animated: true, completion: nil) +// } +// } +// } - func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { - toggleMenu(metadata: metadata, image: image, sender: sender) - } +// func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { +// toggleMenu(metadata: metadata, image: image, sender: sender) +// } - func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { - unifiedSearchMore(metadataForSection: metadataForSection) - } +// func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { +// unifiedSearchMore(metadataForSection: metadataForSection) +// } - func tapRecommendations(with metadata: tableMetadata) { - didSelectMetadata(metadata, withOcIds: false) - } +// func tapRecommendations(with metadata: tableMetadata) { +// didSelectMetadata(metadata, withOcIds: false) +// } func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } @@ -1028,7 +689,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS serverUrlFileName: serverUrlFileName) { _ in } progressHandler: { _, _, fractionCompleted in Task {@MainActor in - LucidBanner.shared.update(progress: fractionCompleted, for: token) + LucidBanner.shared.update( + payload: LucidBannerPayload.Update(progress: fractionCompleted), + for: token + ) } } @@ -1048,14 +712,12 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS fileName: fileName) Task { await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: serverUrl, requestData: true, status: nil) + delegate.transferReloadDataSource(serverUrl: self.serverUrl, requestData: true, status: nil) } } } else { - Task {@MainActor in - await showErrorBanner(scene: scene, - errorDescription: resultsUpload.error.errorDescription, - errorCode: resultsUpload.error.errorCode) + Task { + await showErrorBanner(scene: scene, text: resultsUpload.error.errorDescription) } } } @@ -1066,52 +728,21 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // MARK: - DataSource - @objc func reloadDataSource() { - - // get auto upload folder - autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() - autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: session.urlBase, userId: session.userId, account: session.account) - // get layout for view - layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) - // set GroupField for Grid - if !isSearchingMode && layoutForView?.layout == NCGlobal.shared.layoutGrid { - groupByField = "classFile" - } else { - groupByField = "name" - } - if isSearchingMode { - isDirectoryEncrypted = false - } else { - isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) - } - - DispatchQueue.main.async { -// UIView.transition(with: self.collectionView, -// duration: 0.20, -// options: .transitionCrossDissolve, -// animations: { self.collectionView.reloadData() }, -// completion: nil) -// -// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() - if isRecommendationActived { - Task.detached { + @MainActor + func reloadDataSource() async { + if !isSearchingMode { + Task.detached { + if await self.isRecommendationActived() { await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) } } } - DispatchQueue.main.async { -// UIView.transition(with: self.collectionView, -// duration: 0.20, -// options: .transitionCrossDissolve, -// animations: { self.collectionView.reloadData() }, -// completion: nil) -// -// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() - self.refreshControl.endRefreshing() - self.collectionView.reloadData() - self.setNavigationRightItems() + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: self.serverUrl) } + + await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() } func getServerData(forced: Bool = false) async { } @@ -1165,8 +796,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } completion: { metadatasSearch, error in Task { guard let metadatasSearch, - error == .success, - self.isSearchingMode + error == .success, + self.isSearchingMode else { self.networkSearchInProgress = false await self.reloadDataSource() @@ -1193,7 +824,11 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS guard let metadataForSection = metadataForSection, let lastSearchResult = metadataForSection.lastSearchResult, let cursor = lastSearchResult.cursor, let term = literalSearch else { return } metadataForSection.unifiedSearchInProgress = true - self.collectionView?.reloadData() + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: nil) + } + } self.networking.unifiedSearchFilesProvider(id: lastSearchResult.id, term: term, limit: 5, cursor: cursor, account: session.account) { task in self.searchDataSourceTask = task @@ -1202,12 +837,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } completion: { _, searchResult, metadatas, error in if error != .success { - Task {@MainActor in - await showErrorBanner( - controller: self.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode - ) + Task { + await showErrorBanner(controller: self.controller, text: error.errorDescription) } } @@ -1215,8 +846,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS guard let searchResult = searchResult, let metadatas = metadatas else { return } self.dataSource.appendMetadatasToSection(metadatas, metadataForSection: metadataForSection, lastSearchResult: searchResult) - DispatchQueue.main.async { - self.collectionView?.reloadData() + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: nil) + } } } } @@ -1249,21 +882,38 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } - func pushViewController(viewController: UIViewController) { - if pushed { return } + // MARK: - Header size - pushed = true - navigationController?.pushViewController(viewController, animated: true) - } + func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, + heightHeaderRecommendations: CGFloat, + heightHeaderSection: CGFloat) { + var heightHeaderRichWorkspace: CGFloat = 0 + var heightHeaderRecommendations: CGFloat = 0 + var heightHeaderSection: CGFloat = 0 + + if showDescription, + !isSearchingMode, + let richWorkspaceText = self.richWorkspaceText, + !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { + heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 + } - // MARK: - Header size - - func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { - if headerMenuTransferView, - NCNetworking.shared.isOnline, - let results = database.getResultsMetadatas(predicate: NSPredicate(format: "status IN %@", [global.metadataStatusWaitUpload, global.metadataStatusUploading])), - !results.isEmpty { - return Array(results) + if isRecommendationActived, + !isSearchingMode, + NCPreferences().showRecommendedFiles, + !self.database.getRecommendedFiles(account: self.session.account).isEmpty { + heightHeaderRecommendations = self.heightHeaderRecommendations + heightHeaderSection = self.heightHeaderSection + } + + if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { + if section == 0 { + return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) + } else { + return (0, 0, self.heightHeaderSection) + } + } else { + return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) } } @@ -1308,4 +958,105 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return CGSize(width: collectionView.frame.width, height: 0) } } + + func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } +} + +extension NCCollectionViewCommon: NCSectionFirstHeaderDelegate { + func tapRichWorkspace(_ sender: Any) { + if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { + if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { + viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" + viewerRichWorkspace.serverUrl = serverUrl + viewerRichWorkspace.delegate = self + + navigationController.modalPresentationStyle = .fullScreen + self.present(navigationController, animated: true, completion: nil) + } + } + } + + func tapRecommendations(with metadata: tableMetadata) { + didSelectMetadata(metadata, withOcIds: false) + } +} + +extension NCCollectionViewCommon: NCSectionFooterDelegate { + func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { + unifiedSearchMore(metadataForSection: metadataForSection) + } +} + +extension NCCollectionViewCommon: NCTransferDelegate { + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + + func transferReloadData(serverUrl: String?) { + Task { + await self.debouncerReloadData.call({ + self.collectionView.reloadData() + }, immediate: true) + } + } + + func transferChange(status: String, + account: String, + fileName: String, + serverUrl: String, + selector: String?, + ocId: String, + destination: String?, + error: NKError) { + Task { + if error != .success, + error.errorCode != global.errorResourceNotFound { + await showErrorBanner(controller: self.controller, text: error.errorDescription) + } + guard session.account == account else { + return + } + + if status == self.global.networkingStatusCreateFolder { + if serverUrl == self.serverUrl, + selector != self.global.selectorUploadAutoUpload, + let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { + self.pushMetadata(metadata) + } + return + } + + if self.isSearchingMode { + await self.debouncerNetworkSearch.call { + self.networkSearch() + } + } else if self.serverUrl == serverUrl || destination == self.serverUrl || self.serverUrl.isEmpty { + await self.debouncerReloadDataSource.call { + await self.reloadDataSource() + } + } + } + } + + func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { + Task { + if self.isSearchingMode { + await self.debouncerNetworkSearch.call { + self.networkSearch() + } + return + } + + if requestData && (self.serverUrl == serverUrl || serverUrl == nil) { + await self.debouncerGetServerData.call { + await self.getServerData() + } + return + } + + if self.serverUrl == serverUrl || serverUrl == nil { + await self.debouncerReloadDataSource.call { + await self.reloadDataSource() + } + } + } + } } diff --git a/iOSClient/NCBackgroundLocationUploadManager.swift b/iOSClient/NCBackgroundLocationUploadManager.swift index 3e82c5e4f5..154fda662d 100644 --- a/iOSClient/NCBackgroundLocationUploadManager.swift +++ b/iOSClient/NCBackgroundLocationUploadManager.swift @@ -1,10 +1,6 @@ -// -// NCBackgroundLocationUploadManager.swift -// Nextcloud -// -// Created by Marino Faggiana on 06/06/25. -// Copyright © 2025 Marino Faggiana. All rights reserved. -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import CoreLocation import NextcloudKit @@ -106,16 +102,18 @@ class NCBackgroundLocationUploadManager: NSObject, CLLocationManagerDelegate { } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + // Must work only in background guard isAppInBackground else { return } // Open Realm - if database.openRealmBackground() { - let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! - let location = locations.last - nkLog(tag: self.global.logTagLocation, emoji: .start, message: "Triggered by location change: \(location?.coordinate.latitude ?? 0), \(location?.coordinate.longitude ?? 0)") + guard NCManageDatabase.shared.openRealmBackground() else { + nkLog(tag: self.global.logTagLocation, emoji: .error, message: "Failed to open Realm in Location Manager") + return + } + let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! let location = locations.last nkLog(tag: self.global.logTagLocation, emoji: .start, message: "Triggered by location change: \(location?.coordinate.latitude ?? 0), \(location?.coordinate.longitude ?? 0)") diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index ec0a3d2fb1..4065f60652 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -50,7 +50,7 @@ final class NCGlobal: Sendable { // let avatarSize: Int = 128 * Int(UIScreen.main.scale) let avatarSizeRounded: Int = 128 - + // Preview size // let size1024: CGSize = CGSize(width: 1024, height: 1024) @@ -104,6 +104,7 @@ final class NCGlobal: Sendable { let layoutViewOffline = "LayoutOffline" let layoutViewFavorite = "LayoutFavorite" let layoutViewFiles = "LayoutFiles" + let layoutViewTransfers = "LayoutTransfers" let layoutViewRecent = "LayoutRecent" let layoutViewShares = "LayoutShares" let layoutViewShareExtension = "LayoutShareExtension" @@ -125,7 +126,6 @@ final class NCGlobal: Sendable { let heightFooterButton: CGFloat = 30 let endHeightFooter: CGFloat = 85 - // Text - OnlyOffice - Collabora - QuickLook // let editorText = "text" @@ -229,7 +229,7 @@ final class NCGlobal: Sendable { let selectorSynchronizationOffline = "synchronizationOffline" let selectorPrint = "print" let selectorDeleteFile = "deleteFile" - + // Metadata : Status // // 0 normal @@ -259,6 +259,7 @@ final class NCGlobal: Sendable { let metadataStatusForScreenAwake = [-1, -2, 1, 2] let metadataStatusHideInView = [1, 2, 3, 11] let metadataStatusWaitWebDav = [10, 11, 12, 13, 14, 15] + let metadataStatusTransfers = [-2, -3, 2, 3, 10, 11, 12, 13, 14, 15] let metadatasStatusInWaiting = [-1, 1, 10, 11, 12, 13, 14, 15] let metadatasStatusInWaitingDownloadUpload = [-1, 1] @@ -276,16 +277,22 @@ final class NCGlobal: Sendable { let notificationCenterChangeTheming = "changeTheming" // userInfo: account let notificationCenterRichdocumentGrabFocus = "richdocumentGrabFocus" let notificationCenterReloadDataNCShare = "reloadDataNCShare" - let notificationCenterDidCreateShareLink = "didCreateShareLink" - let notificationCenterCloseRichWorkspaceWebView = "closeRichWorkspaceWebView" let notificationCenterReloadAvatar = "reloadAvatar" let notificationCenterClearCache = "clearCache" let notificationCenterCheckUserDelaultErrorDone = "checkUserDelaultErrorDone" // userInfo: account, controller let notificationCenterServerDidUpdate = "serverDidUpdate" // userInfo: account let notificationCenterNetworkReachability = "networkReachability" + let notificationCenterDidCreateShareLink = "didCreateShareLink" + let notificationCenterDeleteFile = "deleteFile" // userInfo: [ocId], error + let notificationCenterCopyMoveFile = "copyMoveFile" // userInfo: [ocId] serverUrl, account, dragdrop, type (copy, move) + let notificationCenterMoveFile = "moveFile" // userInfo: [ocId], [indexPath], error + let notificationCenterCopyFile = "copyFile" // userInfo: [ocId], [indexPath], error let notificationCenterRenameFile = "renameFile" // userInfo: serverUrl, account, error + let notificationCenterFavoriteFile = "favoriteFile" // userInfo: ocId, serverUrl + let notificationCenterFileExists = "fileExists" // userInfo: ocId, fileExists + let notificationCenterReloadDataSource = "reloadDataSource" // userInfo: serverUrl?, clearDataSource let notificationCenterMenuSearchTextPDF = "menuSearchTextPDF" let notificationCenterMenuGotToPageInPDF = "menuGotToPageInPDF" @@ -293,6 +300,7 @@ final class NCGlobal: Sendable { let notificationCenterOpenMediaDetail = "openMediaDetail" // userInfo: ocId let notificationCenterDismissScanDocument = "dismissScanDocument" + let notificationCenterDismissUploadAssets = "dismissUploadAssets" let notificationCenterEnableSwipeGesture = "enableSwipeGesture" let notificationCenterDisableSwipeGesture = "disableSwipeGesture" @@ -417,7 +425,7 @@ final class NCGlobal: Sendable { let keyFileNameAutoUploadType = "fileNameAutoUploadType" let keyFileNameOriginal = "fileNameOriginal" let keyFileNameOriginalAutoUpload = "fileNameOriginalAutoUpload" - + // LOG TAG // let logTagTask = "BGT" @@ -436,6 +444,10 @@ final class NCGlobal: Sendable { // let udMigrationMultiDomains = "migrationMultiDomains" let udLastVersion = "lastVersion" + + // Album + // + let selectedTabIndexAlbum: Int = 3 } /** diff --git a/iOSClient/Networking/NCConfigServer.swift b/iOSClient/Networking/NCConfigServer.swift index d5e4d2a867..f82b466647 100644 --- a/iOSClient/Networking/NCConfigServer.swift +++ b/iOSClient/Networking/NCConfigServer.swift @@ -1,25 +1,6 @@ -// -// NCConfigServer.swift -// Nextcloud -// -// Created by Marino Faggiana on 05/12/22. -// Copyright © 2022 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2022 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import UIKit diff --git a/iOSClient/Networking/NCNetworking+Download.swift b/iOSClient/Networking/NCNetworking+Download.swift index 22ecb91653..4d9fe0c51c 100644 --- a/iOSClient/Networking/NCNetworking+Download.swift +++ b/iOSClient/Networking/NCNetworking+Download.swift @@ -1,25 +1,6 @@ -// -// NCNetworking+Download.swift -// Nextcloud -// -// Created by Marino Faggiana on 07/02/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit import NextcloudKit @@ -346,40 +327,3 @@ extension NCNetworking { } } } - -class NCOperationDownload: ConcurrentOperation, @unchecked Sendable { - var metadata: tableMetadata - var selector: String - - init(metadata: tableMetadata, selector: String) { - self.metadata = tableMetadata.init(value: metadata) - self.selector = selector - } - - override func start() { - guard !isCancelled else { return self.finish() } - - metadata.session = NCNetworking.shared.sessionDownload - metadata.sessionError = "" - metadata.sessionSelector = selector - metadata.sessionTaskIdentifier = 0 - metadata.status = NCGlobal.shared.metadataStatusWaitDownload - -// let metadata = NCManageDatabase.shared.addMetadata(metadata) - -// NCNetworking.shared.download(metadata: metadata, withNotificationProgressTask: true) { -// } completion: { _, _ in -// self.finish() -// } - Task { - await download(withSelector: self.selector) - } - } - - private func download(withSelector selector: String = "") async { - await NCNetworking.shared.downloadFile(metadata: metadata) { _ in - self.finish() - } taskHandler: { _ in } - - } -} diff --git a/iOSClient/Networking/NCNetworking+LivePhoto.swift b/iOSClient/Networking/NCNetworking+LivePhoto.swift index 072b3b087f..e861b76b81 100644 --- a/iOSClient/Networking/NCNetworking+LivePhoto.swift +++ b/iOSClient/Networking/NCNetworking+LivePhoto.swift @@ -1,25 +1,6 @@ -// -// NCNetworking+LivePhoto.swift -// Nextcloud -// -// Created by Marino Faggiana on 07/02/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit import NextcloudKit diff --git a/iOSClient/Networking/NCNetworking+Task.swift b/iOSClient/Networking/NCNetworking+Task.swift index 1b6122252b..f28830b583 100644 --- a/iOSClient/Networking/NCNetworking+Task.swift +++ b/iOSClient/Networking/NCNetworking+Task.swift @@ -1,25 +1,6 @@ -// -// NCNetworking+Task.swift -// Nextcloud -// -// Created by Marino Faggiana on 24/08/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import UIKit diff --git a/iOSClient/Networking/NCNetworking+Upload.swift b/iOSClient/Networking/NCNetworking+Upload.swift index 72bbffdf21..9467e9c0b7 100644 --- a/iOSClient/Networking/NCNetworking+Upload.swift +++ b/iOSClient/Networking/NCNetworking+Upload.swift @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + import UIKit import NextcloudKit import Alamofire @@ -41,22 +45,6 @@ extension NCNetworking { } taskHandler(task) } progressHandler: { progress in - Task { - guard await self.progressQuantizer.shouldEmit(serverUrlFileName: serverUrlFileName, fraction: progress.fractionCompleted) else { - return - } - - if let metadata { - await NCManageDatabase.shared.setMetadataProgress(ocId: metadata.ocId, progress: progress.fractionCompleted) - await self.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferProgressDidUpdate(progress: Float(progress.fractionCompleted), - totalBytes: progress.totalUnitCount, - totalBytesExpected: progress.completedUnitCount, - fileName: metadata.fileName, - serverUrl: metadata.serverUrl) - } - } - } progressHandler(progress.completedUnitCount, progress.totalUnitCount, progress.fractionCompleted) } @@ -310,7 +298,11 @@ extension NCNetworking { #if !EXTENSION await NCNetworking.shared.setLivePhoto(account: metadata.account) #endif - } + } else { +#if !EXTENSION + AnalyticsHelper.shared.trackEventWithMetadata(eventName: .EVENT__UPLOAD_FILE ,metadata: metadata) +#endif + } await self.transferDispatcher.notifyAllDelegates { delegate in delegate.transferChange(status: self.global.networkingStatusUploaded, @@ -386,7 +378,6 @@ extension NCNetworking { error: self.global.diagnosticProblemsUploadServerError) } } - await NCManageDatabase.shared.updateBadge() } // MARK: - diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 6eabc1aa6b..37629dadd1 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -1,25 +1,6 @@ -// -// NCNetworking+WebDAV.swift -// Nextcloud -// -// Created by Marino Faggiana on 07/02/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit import NextcloudKit @@ -454,7 +435,7 @@ extension NCNetworking { return results.error } - + // MARK: - Rename func setStatusWaitRename(_ metadata: tableMetadata, fileNameNew: String, windowScene: UIWindowScene?) async -> NKError { @@ -484,7 +465,40 @@ extension NCNetworking { return .success } - + + func renameFileOrFolder(metadata: tableMetadata) async -> NKError { + let serverUrlFileNameSource = metadata.serverUrlFileName + let serverUrlFileNameDestination = utilityFileSystem.createServerUrl(serverUrl: metadata.serverUrl, fileName: metadata.fileName) + + let results = await NextcloudKit.shared.moveFileOrFolderAsync(serverUrlFileNameSource: serverUrlFileNameSource, serverUrlFileNameDestination: serverUrlFileNameDestination, overwrite: false, account: metadata.account) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: metadata.account, + path: serverUrlFileNameSource, + name: "moveFileOrFolder") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } + + if results.error == .success { + await NCManageDatabase.shared.setMetadataServerUrlFileNameStatusNormalAsync(ocId: metadata.ocId) + } else { + await NCManageDatabase.shared.restoreMetadataFileNameAsync(ocId: metadata.ocId) + } + + await transferDispatcher.notifyAllDelegates { delegate in + delegate.transferChange(status: NCGlobal.shared.networkingStatusRename, + account: metadata.account, + fileName: metadata.fileName, + serverUrl: metadata.serverUrl, + selector: metadata.sessionSelector, + ocId: metadata.ocId, + destination: nil, + error: results.error) + } + + return results.error + } + func renameMetadata(_ metadata: tableMetadata, fileNameNew: String, indexPath: IndexPath, @@ -516,52 +530,20 @@ extension NCNetworking { #endif } else { Task { + let ocId = metadata.ocId + let serverUrl = metadata.serverUrl await self.transferDispatcher.notifyAllDelegatesAsync { delegate in - let status = self.global.metadataStatusWaitRename - await NCManageDatabase.shared.renameMetadata(fileNameNew: fileNameNew, ocId: metadata.ocId, status: status) - delegate.transferReloadData(serverUrl: metadata.serverUrl, status: status) + await NCManageDatabase.shared.renameMetadata(fileNameNew: fileNameNew, ocId: ocId, status: self.global.metadataStatusWaitRename) + delegate.transferReloadDataSource(serverUrl: serverUrl, requestData: false, status: self.global.metadataStatusWaitRename) } - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterRenameFile, userInfo: ["serverUrl": metadata.serverUrl, "account": metadata.account, "error": NKError(errorCode: 0, errorDescription: ""), "ocId": metadata.ocId, "indexPath": indexPath]) +// NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterRenameFile, userInfo: ["serverUrl": metadata.serverUrl, "account": metadata.account, "error": NKError(errorCode: 0, errorDescription: ""), "ocId": metadata.ocId, "indexPath": indexPath]) } completion(NKError(errorCode: 0, errorDescription: "")) } } - - func renameFileOrFolder(metadata: tableMetadata) async -> NKError { - let serverUrlFileNameSource = metadata.serverUrlFileName - let serverUrlFileNameDestination = utilityFileSystem.createServerUrl(serverUrl: metadata.serverUrl, fileName: metadata.fileName) - - let results = await NextcloudKit.shared.moveFileOrFolderAsync(serverUrlFileNameSource: serverUrlFileNameSource, serverUrlFileNameDestination: serverUrlFileNameDestination, overwrite: false, account: metadata.account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: metadata.account, - path: serverUrlFileNameSource, - name: "moveFileOrFolder") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } - - if results.error == .success { - await NCManageDatabase.shared.setMetadataServerUrlFileNameStatusNormalAsync(ocId: metadata.ocId) - } else { - await NCManageDatabase.shared.restoreMetadataFileNameAsync(ocId: metadata.ocId) - } - - await transferDispatcher.notifyAllDelegates { delegate in - delegate.transferChange(status: NCGlobal.shared.networkingStatusRename, - account: metadata.account, - fileName: metadata.fileName, - serverUrl: metadata.serverUrl, - selector: metadata.sessionSelector, - ocId: metadata.ocId, - destination: nil, - error: results.error) - } - - return results.error - } - + // MARK: - Move func setStatusWaitMove(_ metadata: tableMetadata, destination: String, overwrite: Bool) async -> NKError { @@ -702,8 +684,15 @@ extension NCNetworking { let serverUrl = metadata.serverUrl let favorite = metadata.favorite await self.transferDispatcher.notifyAllDelegatesAsync { delegate in + +#if !EXTENSION + if !metadata.favorite, !metadata.contentType.contains("directory") { + AnalyticsHelper.shared.trackEventWithMetadata(eventName: .EVENT__ADD_FAVORITE ,metadata: metadata) + } +#endif await NCManageDatabase.shared.setMetadataFavoriteAsync(ocId: ocId, favorite: !favorite, saveOldFavorite: favorite.description, status: self.global.metadataStatusWaitFavorite) delegate.transferReloadDataSource(serverUrl: serverUrl, requestData: false, status: self.global.metadataStatusWaitFavorite) + } return .success @@ -874,3 +863,4 @@ class NCOperationDownloadAvatar: ConcurrentOperation, @unchecked Sendable { } } } + diff --git a/iOSClient/Networking/NCNetworking.swift b/iOSClient/Networking/NCNetworking.swift index 7868b64db4..c0684756d4 100644 --- a/iOSClient/Networking/NCNetworking.swift +++ b/iOSClient/Networking/NCNetworking.swift @@ -37,203 +37,6 @@ protocol NCTransferDelegate: AnyObject { serverUrl: String) } -extension NCTransferDelegate { - func transferChange(status: String, - account: String, - fileName: String, - serverUrl: String, - selector: String?, - ocId: String, - destination: String?, - error: NKError) {} - func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) {} - func transferProgressDidUpdate(progress: Float, - totalBytes: Int64, - totalBytesExpected: Int64, - fileName: String, - serverUrl: String) {} -} - -/// Actor-based delegate dispatcher using weak references. -actor NCTransferDelegateDispatcher { - // Weak reference collection of delegates - private var transferDelegates = NSHashTable.weakObjects() - - /// Adds a delegate safely. - func addDelegate(_ delegate: NCTransferDelegate) { - transferDelegates.add(delegate) - } - - /// Remove a delegate safely. - func removeDelegate(_ delegate: NCTransferDelegate) { - transferDelegates.remove(delegate) - } - - /// Notifies all delegates. - func notifyAllDelegates(_ block: (NCTransferDelegate) -> Void) { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - block(delegate) - } - } - - func notifyAllDelegatesAsync(_ block: @escaping (NCTransferDelegate) async -> Void) async { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - await block(delegate) - } - } - - /// Notifies the delegate for a specific scene. - func notifyDelegate(forScene sceneIdentifier: String, _ block: (NCTransferDelegate) -> Void) { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - if delegate.sceneIdentifier == sceneIdentifier { - block(delegate) - } - } - } - - /// Notifies matching and non-matching delegates for a specific scene. - func notifyDelegates(forScene sceneIdentifier: String, - matching: (NCTransferDelegate) -> Void, - others: (NCTransferDelegate) -> Void) { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - if delegate.sceneIdentifier == sceneIdentifier { - matching(delegate) - } else { - others(delegate) - } - } - } -} - -/// A thread-safe registry for tracking in-flight `URLSessionTask` instances. -/// -/// Each task is associated with a string identifier (`identifier`) that you define, -/// allowing you to check whether a request is already running, avoid duplicates, -/// and cancel all active tasks at once. The registry automatically removes -/// completed tasks via `cleanupCompleted()` to keep memory usage compact. -/// -/// Typical use cases: -/// - Ensure only one task per identifier is active at a time. -/// - Query whether a specific request is still running (`isReading`). -/// - Forcefully stop a specific request (`cancel`). -/// - Forcefully stop all tasks when leaving a screen (`cancelAll`). -actor NetworkingTasks { - private var active: [(identifier: String, task: URLSessionTask)] = [] - - /// Returns whether there is an in-flight task for the given URL. - /// - /// A task is considered in-flight if its `state` is `.running` or `.suspended`. - /// - Parameter identifier: The identifier to check. - /// - Returns: `true` if a matching in-flight task exists; otherwise `false`. - func isReading(identifier: String) -> Bool { - // Drop finished/canceling tasks globally - cleanup() - - return active.contains { - $0.identifier == identifier && ($0.task.state == .running || $0.task.state == .suspended) - } - } - - /// Tracks a newly created `URLSessionTask` for the given identifier. - /// - /// If a running entry for the same identifier exists, it is removed before appending the new one. - /// - Parameters: - /// - identifier: The identifier associated with the task. - /// - task: The `URLSessionTask` to track. - func track(identifier: String, task: URLSessionTask) { - // Drop finished/canceling tasks globally - cleanup() - - active.removeAll { - $0.identifier == identifier && $0.task.state == .running - } - active.append((identifier, task)) - nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .start, message: "Start task for identifier: \(identifier)", consoleOnly: true) - } - - /// create a Identifier - /// - func createIdentifier(account: String? = nil, path: String? = nil, name: String) -> String { - if let account, - let path { - return account + "_" + path + "_" + name - } else if let path { - return path + "_" + name - } else { - return name - } - } - - /// Cancels and removes all tasks associated with the given id. - /// - /// - Parameter identifier: The identifier whose tasks should be canceled. - func cancel(identifier: String) { - // Drop finished/canceling tasks globally - cleanup() - - for element in active where element.identifier == identifier { - element.task.cancel() - nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .cancel, message: "Cancel task for identifier: \(identifier)", consoleOnly: true) - } - active.removeAll { - $0.identifier == identifier - } - } - - /// Cancels all tracked `URLSessionTask` and clears the registry. - /// - /// Call this when leaving the page/screen or when the operation must be forcefully stopped. - func cancelAll() { - active.forEach { - $0.task.cancel() - nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .cancel, message: "Cancel task with identifier: \($0.identifier)", consoleOnly: true) - } - active.removeAll() - } - - /// Removes tasks that have completed from the registry. - /// - /// Useful to keep the in-memory list compact during long-running operations. - func cleanup() { - active.removeAll { - $0.task.state == .completed || $0.task.state == .canceling - } - } -} - -/// Quantizes per-task progress updates to integer percentages (0...100). -/// Each (serverUrlFileName) pair is tracked separately, so you get -/// at most one update per integer percent for each transfer. -actor ProgressQuantizer { - private var lastPercent: [String: Int] = [:] - - /// Returns `true` only when integer percent changes (or hits 100). - /// - /// - Parameters: - /// - serverUrlFileName: The name of the file being transferred. - /// - fraction: Progress fraction [0.0 ... 1.0]. - func shouldEmit(serverUrlFileName: String, fraction: Double) -> Bool { - let percent = min(max(Int((fraction * 100).rounded(.down)), 0), 100) - - let last = lastPercent[serverUrlFileName] ?? -1 - guard percent != last || percent == 100 else { - return false - } - - lastPercent[serverUrlFileName] = percent - return true - } - - /// Clears stored state for a finished transfer. - func clear(serverUrlFileName: String) { - lastPercent.removeValue(forKey: serverUrlFileName) - } -} - class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { static let shared = NCNetworking() @@ -289,8 +92,8 @@ class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { let downloadThumbnailTrashQueue = Queuer(name: "downloadThumbnailTrashQueue", maxConcurrentOperationCount: 10, qualityOfService: .default) let saveLivePhotoQueue = Queuer(name: "saveLivePhotoQueue", maxConcurrentOperationCount: 1, qualityOfService: .default) let downloadAvatarQueue = Queuer(name: "downloadAvatarQueue", maxConcurrentOperationCount: 10, qualityOfService: .default) + let fileExistsQueue = Queuer(name: "fileExistsQueue", maxConcurrentOperationCount: 10, qualityOfService: .default) #endif - let downloadQueue = Queuer(name: "downloadQueue", maxConcurrentOperationCount: NCBrandOptions.shared.httpMaximumConnectionsPerHostInDownload, qualityOfService: .default) // MARK: - init diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index 9121911b0b..930a965320 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -271,6 +271,7 @@ actor NCNetworkingProcess { await startTimer(interval: minInterval) } } else { + // Remove upload asset await removeUploadedAssetsIfNeeded() // Set Live Photo diff --git a/iOSClient/Networking/NCService.swift b/iOSClient/Networking/NCService.swift index 2987e2543c..f6715693d3 100644 --- a/iOSClient/Networking/NCService.swift +++ b/iOSClient/Networking/NCService.swift @@ -1,25 +1,6 @@ -// -// NCService.swift -// Nextcloud -// -// Created by Marino Faggiana on 14/03/18. -// Copyright © 2018 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2018 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit @preconcurrency import NextcloudKit diff --git a/iOSClient/Settings/PrivacyPolicyViewController.swift b/iOSClient/Settings/PrivacyPolicyViewController.swift index 141c444fca..9e1bd0ed64 100644 --- a/iOSClient/Settings/PrivacyPolicyViewController.swift +++ b/iOSClient/Settings/PrivacyPolicyViewController.swift @@ -35,6 +35,10 @@ class PrivacyPolicyViewController: UIViewController, WKNavigationDelegate, WKUID override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + + // Re-evaluate in-app messages after viewDidAppear + MoEngageAnalytics.shared.displayInAppNotificationSafely(reason: "viewDidAppear") + myWebView = WKWebView(frame: CGRect(x:0, y:0, width: UIScreen.main.bounds.width, height:UIScreen.main.bounds.height)) } override func didReceiveMemoryWarning() { diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index b3993620ba..757d5532d5 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -13,7 +13,7 @@ import XLForm class PrivacySettingsViewController: XLFormViewController{ - @objc public var isShowSettingsButton: Bool = false + @objc public var isShowSettingsButton: Bool = false override func viewDidLoad() { super.viewDidLoad() @@ -36,6 +36,8 @@ class PrivacySettingsViewController: XLFormViewController{ initializeForm() } + + //MARK: XLForm func initializeForm() { @@ -56,6 +58,7 @@ class PrivacySettingsViewController: XLFormViewController{ section.footerTitle = NSLocalizedString("_privacy_settings_help_text_", comment: "") form.addFormSection(section) + //custom cell section = XLFormSectionDescriptor.formSection(withTitle: "") section.footerTitle = NSLocalizedString("_required_data_collection_help_text_", comment: "") @@ -64,6 +67,7 @@ class PrivacySettingsViewController: XLFormViewController{ XLFormViewController.cellClassesForRowDescriptorTypes()["RequiredDataCollectionCustomCellType"] = RequiredDataCollectionSwitch.self + row = XLFormRowDescriptor(tag: "ButtonDestinationFolder", rowType: "RequiredDataCollectionCustomCellType", title: "") row.cellConfig["requiredDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue @@ -78,6 +82,7 @@ class PrivacySettingsViewController: XLFormViewController{ XLFormViewController.cellClassesForRowDescriptorTypes()["AnalysisDataCollectionCustomCellType"] = AnalysisDataCollectionSwitch.self + row = XLFormRowDescriptor(tag: "AnalysisDataCollectionSwitch", rowType: "AnalysisDataCollectionCustomCellType", title: "") row.cellConfig["analysisDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue @@ -92,11 +97,13 @@ class PrivacySettingsViewController: XLFormViewController{ section.addFormRow(row) + XLFormViewController.cellClassesForRowDescriptorTypes()["SaveSettingsButton"] = SaveSettingsCustomButtonCell.self section = XLFormSectionDescriptor.formSection(withTitle: "") form.addFormSection(section) + row = XLFormRowDescriptor(tag: "SaveSettingsButton", rowType: "SaveSettingsButton", title: "") row.cellConfig["backgroundColor"] = UIColor.clear diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 8be6601682..cd2a633acc 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -126,8 +126,8 @@ "_error_parameter_schema_" = "Wrong parameters, impossible to continue"; "_comments_" = "Comments"; "_sharing_" = "Sharing"; -//"_details_" = "Share"; -"_details_" = "Details"; +"_details_" = "Share"; +//"_details_" = "Details"; "_sub_details_" = "Subscription Details"; "_subscriptions_" = "Subscriptions"; "_dark_mode_" = "Dark mode"; @@ -232,6 +232,8 @@ "_settings_account_request_" = "Request account at startup"; "_print_" = "Print"; "_alias_" = "Alias"; +//"_alias_placeholder_" = "Write alias"; +//"_alias_footer_" = "Give your accounts a descriptive name such as Home, Office, School…"; "_alias_placeholder_" = "Write the alias"; "_alias_footer_" = "Give your account names a descriptive name such as Home, Office, School …"; "_chunk_size_mb_" = "Chunk size in MB"; @@ -281,7 +283,7 @@ //TOPasscodeSettingsWarningLabel.m "_1_failed_passcode_attempt" = "1 Failed Passcode Attempt"; -"_failed_passcode_attempts" = "%d Failed Passcode Attempts"; +"_failed_passcode_attempts" = "%ld Failed Passcode Attempts"; /* Background of the file listing view */ "_use_as_background_" = "Use it as a background"; @@ -322,6 +324,7 @@ "_status_message_" = "Status message"; "_status_message_placehorder_" = "What is your status?"; "_online_status_" = "Online status"; +"_clear_status_message_" = "Clear status message"; "_set_status_message_" = "Set status message"; "_clear_status_message_after_" = "Clear status after"; @@ -384,8 +387,8 @@ "_username_" = "Username"; "_change_credentials_" = "Change your credentials"; "_wifi_only_" = "Only use Wi-Fi connection"; -"_settings_autoupload_" = "Auto upload photos"; -//"_settings_autoupload_" = "Auto upload"; +//"_settings_autoupload_" = "Auto upload photos"; +"_settings_autoupload_" = "Auto upload"; "_app_version_" = "Application version"; "_app_in_use_" = "Application in use"; "_contact_by_email_" = "Contact us by email"; @@ -482,6 +485,7 @@ "_autoupload_change_location_footer_" = "Change folder used for \"Automatic upload of camera photos\" (if the option is enabled)"; "_autoupload_not_select_home_" = "Select a folder"; "_autoupload_save_album_" = "Copy photo or video into the photo album"; +"_autoupload_description_" = "Choose whether new photos or videos will be automatically uploaded to your account."; "_autoupload_fullphotos_" = "Upload the whole camera roll"; "_start_autoupload_" = "Turn on auto upload"; "_stop_autoupload_" = "Turn off auto upload"; @@ -625,9 +629,11 @@ "_add_offline_" = "Add to offline"; "_remove_passcode_" = "Remove password protection"; "_protect_passcode_" = "Protect with password"; +"_remove_favorites_" = "Unfavorite"; +"_add_favorites_" = "Favorite"; "_share_" = "Share"; "_reload_" = "Reload"; -"_open_in_" = "Open in …"; +"_open_in_" = "Open with..."; //"Open in …"; "_open_" = "Open …"; "_remove_local_file_" = "Remove locally"; "_add_local_" = "Add to local storage"; @@ -714,7 +720,7 @@ "_status_upload_error_" = "Error, waiting to upload"; "_select_media_folder_" = "Set Media folder"; "_media_viewimage_show_" = "Show only images"; -"_media_viewvideo_show_" = "Show only video"; +"_media_viewvideo_show_" = "Show only videos"; "_media_show_all_" = "Show both"; "_media_by_created_date_" = "Sort by created date"; "_media_by_upload_date_" = "Sort by upload date"; @@ -735,10 +741,12 @@ "_share_link_" = "Share link"; //"_share_link_" = "Share link"; "_share_link_" = "Link"; +//"_share_link_" = "Share link"; "_share_link_button_" = "Send link to …"; "_share_link_name_" = "Link name"; "_password_" = "Password"; "_share_password_" = "Password protected link"; +"_share_password_must_be_at_least_6_chars" = "Password must be at least 6 characters."; "_share_expirationdate_" = "Set expiration date for link"; "_date_" = "Date"; "_share_title_" = "Share"; @@ -796,6 +804,42 @@ "_new_comment_" = "New comment …"; "_edit_comment_" = "Edit comment"; "_delete_comment_" = "Delete comment"; +"_share_read_only_" = "View only"; +"_share_reshare_allowed_" = "Resharing is allowed."; +"_share_reshare_not_allowed_" = "Resharing is not allowed."; +"_sharing_message_" = "You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration."; +"_create_link_" = "Create Link"; +"personal_share_by_mail" = "Personal share by mail"; +"_your_shares_" = "Your Shares"; +"_share_linklabel_" = "Link '%@'"; +"_share_link_folder_" = "Link to folder"; +"_share_link_file_" = "Link to file"; +"no_shares_created" = "No shares created yet."; +"_advance_permissions_" = "Advanced permissions"; +"_send_new_email_" = "Send new email"; +"_apply_changes_" = "Apply changes"; +"_send_share_" = "Send share"; +"_PERMISSIONS_" = "PERMISSIONS"; +"share_editing_message" = "There are no editing functions for files unless you share them with MagentaCLOUD users."; +"_file_drop_message_" = "With File drop, only uploading is allowed. Only you can see files and folders that have been uploaded."; +"_custom_link_label" = "Your custom link label"; +"_set_password_" = "Set password"; +"_share_expiration_date_placeholder_"= "Expiration date for this share"; +"_share_download_limit_" = "Download Limit"; +"_share_download_limit_placeholder_" = "Enter download limit"; +"_share_download_limit_alert_empty_" = "Download limit cannot be empty."; +"_share_download_limit_alert_zero_" = "Download limit should be greater than 0."; +"_share_download_limit_alert_invalid_" = "Please enter a valid number for the download limit."; +"_share_remaining_download_" = "Downloads:"; +"_share_read_only_" = "View only"; +"_share_remaining_download_" = "Downloads:"; +"_share_editing_" = "Can edit"; +"_share_file_drop_" = "Filedrop only"; +"_share_hide_download_" = "Prevent download"; +//"_share_note_recipient_" = "YOUR MESSAGE"; +"_share_note_recipient_" = "Note to recipient"; +"_shareLinksearch_placeholder_" = "Contact name or email address"; +"_shareLinksearch_mail_placeholder_" = "Type a name or an email and press Enter"; "_share_reshare_allowed_" = "Resharing is allowed."; "_share_reshare_not_allowed_" = "Resharing is not allowed."; "_sharing_message_" = "You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration."; @@ -865,6 +909,7 @@ "_share_via_link_menu_password_label_" = "Password protect (%1$s)"; "_share_link_empty_exp_date_" = "You must select expiration date."; "_share_link_empty_note_message_" = "Please enter note."; +"_share_permission_should_not_be_empty_" = "Please select at least one permission."; "_remote_" = "Remote"; "_remote_group_" = "Remote group"; "_conversation_" = "Conversation"; @@ -1097,11 +1142,11 @@ "_voice_memo_stop_" = "Tap to stop"; "_voice_memo_filename_" = "Voice memo"; "_voice_memo_title_" = "Upload voice memo"; -"Enter Passcode" = "Enter Passcode"; -"Enter a new passcode" = "Enter a new passcode"; -"Confirm new passcode" = "Confirm new passcode"; +//"Enter Passcode" = "Enter Passcode"; +//"Enter a new passcode" = "Enter a new passcode"; +//"Confirm new passcode" = "Confirm new passcode"; "_too_many_failed_passcode_attempts_error_" = "Too many failed passcode attempts. Please try again."; -"Passcodes didn't match. Try again." = "Passcodes didn't match. Try again."; +//"Passcodes didn't match. Try again." = "Passcodes didn't match. Try again."; "Delete" = "Delete"; "Cancel" = "Cancel"; "_certificate_details_" = "Certificate Server details"; @@ -1160,7 +1205,7 @@ "_waiting_for_" = "Waiting for:"; "_waiting_" = "Waiting …"; "_reachable_wifi_" = "network reachable via Wi-Fi or cable"; -"_ITMS-90076_" = "Due to a change in the Nextcloud application identifier, the settings and password for accessing your cloud are reset, so please re-enter your account data and check your Settings. We are sorry about that."; +"_ITMS-90076_" = "Due to a change in the MagentaCLOUD application identifier, the settings and password for accessing your cloud are reset, so please re-enter your account data and check your Settings. We are sorry about that."; "_password_not_present_" = "Please re-insert your credentials."; "_copy_passphrase_" = "Copy passphrase"; "_ok_copy_passphrase_" = "OK and copy passphrase"; @@ -1318,7 +1363,7 @@ "_delete_in_progress_" = "Delete in progress …"; "_download_in_progress_" = "Download in progress …"; "_upload_in_progress_" = "Upload in progress …"; -"_transfer_in_progress_" = "Transfer in progress …"; +"_transfer_in_progress_" = "Transfer in progress …"; "_in_waiting_" = "In waiting"; "_in_progress_" = "In progress"; "_in_error_" = "In error"; @@ -1533,3 +1578,46 @@ You can stop it at any time, adjust the settings, and enable it again."; "_e2ee_setup_store_privatekey_" = "Private key generation returned no usable key"; "_e2ee_setup_verify_publickey_" = "Public key does not match the certificate"; "_e2ee_setup_generate_passphrase_" = "Unable to generate a passphrase"; + +// MARK: Albums +"_albums_loading_popup_desc_" = "Wait a moment..."; +"_albums_list_nav_title_" = "Albums"; +"_albums_list_new_album_btn_" = "New"; +"_albums_list_loading_msg_" = "Loading..."; +"_albums_list_error_msg_" = "Unable to load albums. Please try again later!"; +"_albums_list_album_name_validation_nonempty_" = "Album name cannot be empty."; +"_albums_list_album_name_validation_min_length_" = "Album name must be at least 3 characters."; +"_albums_list_album_name_validation_max_length_" = "Album name cannot be more than 30 characters."; +"_albums_list_album_name_validation_specials_" = "Album name cannot contain slashes."; +"_albums_list_empty_heading_" = "Create\nAlbums\nfor your\nPhotos"; +"_albums_list_empty_subheading_" = "You can organize all your photos in as many albums as you like. You haven't created an album yet."; +"_albums_list_empty_new_album_btn_" = "Create album"; +"_albums_list_own_albums_heading_" = "My albums"; +"_albums_list_entities_" = "Items"; +"_albums_list_new_album_popup_title_" = "Create new Album"; +"_albums_list_new_album_popup_desc_" = "Please enter an album name between 3 and 30 characters."; +"_albums_list_new_album_popup_hint_" = "Album's name"; +"_albums_list_new_album_popup_positive_btn_" = "Create"; +"_albums_list_new_album_popup_negative_btn_" = "Cancel"; +"_albums_list_rename_album_popup_title_" = "Rename Album"; +"_albums_list_rename_album_popup_desc_" = "Please enter new album name between 3 and 30 characters."; +"_albums_list_rename_album_popup_hint_" = "Album's new name"; +"_albums_list_rename_album_popup_positive_btn_" = "Rename"; +"_albums_list_rename_album_popup_negative_btn_" = "Cancel"; +"_albums_photo_selection_sheet_title_" = "Select items"; +"_albums_photo_selection_sheet_back_btn_" = "Back"; +"_albums_photo_selection_sheet_done_btn_" = "Done"; +"_albums_photos_loading_msg_" = "Loading photos..."; +"_albums_photos_error_msg_" = "Unable to load photos. Please try again later!"; +"_albums_photos_empty_heading_" = "All that's\nmissing are\nyour photos"; +"_albums_photos_empty_subheading_" = "You can add as many photos as you like. A photo can also belong to more than one album."; +"_albums_photos_empty_add_photos_btn_" = "Add photos"; +"_albums_photos_add_photos_btn_" = "Add"; +"_albums_photos_rename_album_btn_" = "Rename Album"; +"_albums_photos_delete_album_btn_" = "Delete Album"; +"_albums_delete_album_popup_title_" = "Delete Album?"; +"_albums_delete_album_popup_desc_" = "Are you sure you want to delete this album? This action cannot be undone."; +"_albums_delete_album_popup_positive_btn_" = "Delete"; +"_albums_delete_album_popup_negative_btn_" = "Cancel"; +"_add_to_album" = "Add to Album"; +"_remove_from_album_" = "Remove from Album"; From 3a680a440ca4a6344e6817c0b3e28aa0677a8942 Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:58:19 +0530 Subject: [PATCH 13/40] NMC 1984 - Privacy policy customisation added --- .../NCCollectionViewCommon.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index f8d9b7cefe..a483ad4d4b 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -229,6 +229,14 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let dropInteraction = UIDropInteraction(delegate: self) self.navigationController?.navigationItem.leftBarButtonItems?.first?.customView?.addInteraction(dropInteraction) + + if(!UserDefaults.standard.bool(forKey: "isInitialPrivacySettingsShowed") || isApplicationUpdated()){ + redirectToPrivacyViewController() + + //set current app version + let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String + UserDefaults.standard.set(appVersion, forKey: "CurrentAppVersion") + } if(!UserDefaults.standard.bool(forKey: "isInitialPrivacySettingsShowed") || isApplicationUpdated()){ redirectToPrivacyViewController() @@ -341,6 +349,19 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, removeImageCache(metadatas: self.dataSource.getMetadatas()) } + + func isApplicationUpdated() -> Bool{ + let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" + let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") + return currentVersion != appVersion + } + + func redirectToPrivacyViewController(){ + let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) + let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController + newViewController.modalPresentationStyle = .fullScreen + self.present(newViewController, animated: true, completion: nil) + } func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { let viewController = presentationController.presentedViewController From 6b1073a609e6900b997393a612e91727ab2d99b2 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Wed, 9 Apr 2025 14:10:39 +0530 Subject: [PATCH 14/40] NMC 1984 - Privacy policy customisation changes added --- ...mon+CollectionViewDelegateFlowLayout.swift | 43 + .../NCCollectionViewCommon.swift | 1401 +++++++++++------ 2 files changed, 954 insertions(+), 490 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift index 06795639bc..42bbbd6038 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift @@ -13,4 +13,47 @@ extension NCCollectionViewCommon: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { return sizeForFooterInSection(section: section) } + + func getHeaderHeight() -> CGFloat { + + var size: CGFloat = 0 + // transfer in progress + if headerMenuTransferView, + let metadata = NCManageDatabase.shared.getMetadataFromOcId(NCNetworking.shared.transferInForegorund?.ocId), + metadata.isTransferInForeground { + if !isSearchingMode { + size += NCGlobal.shared.heightHeaderTransfer + } + } else { + NCNetworking.shared.transferInForegorund = nil + } + + if headerMenuButtonsView { + size += NCGlobal.shared.heightButtonsView + } + + return size + } + + func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { + + var headerRichWorkspace: CGFloat = 0 + + if let richWorkspaceText = richWorkspaceText, showDescription { + let trimmed = richWorkspaceText.trimmingCharacters(in: .whitespaces) + if !trimmed.isEmpty && !isSearchingMode { + headerRichWorkspace = UIScreen.main.bounds.size.height / 6 + } + } + + if isSearchingMode || layoutForView?.layout == NCGlobal.shared.layoutGrid || dataSource.numberOfSections() > 1 { + if section == 0 { + return (getHeaderHeight(), headerRichWorkspace, NCGlobal.shared.heightSection) + } else { + return (0, 0, NCGlobal.shared.heightSection) + } + } else { + return (getHeaderHeight(), headerRichWorkspace, 0) + } + } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index a483ad4d4b..5ce5eb5132 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -10,123 +10,165 @@ import EasyTipView import LucidBanner class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { +//class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionHeaderMenuDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate, NCEmptyDataSetDelegate { @IBOutlet weak var collectionView: UICollectionView! - let database = NCManageDatabase.shared - let global = NCGlobal.shared - let utility = NCUtility() - let utilityFileSystem = NCUtilityFileSystem() - let imageCache = NCImageCache.shared - var dataSource = NCCollectionViewDataSource() - let networking = NCNetworking.shared - let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! - var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() - - var autoUploadFileName = "" - var autoUploadDirectory = "" - let refreshControl = UIRefreshControl() - var searchController: UISearchController? - var backgroundImageView = UIImageView() - var serverUrl: String = "" - var isEditMode = false - var isDirectoryE2EE = false - var fileSelect: [String] = [] - var metadataFolder: tableMetadata? - var richWorkspaceText: String? - var sectionFirstHeader: NCSectionFirstHeader? - var sectionFirstHeaderEmptyData: NCSectionFirstHeaderEmptyData? - var isSearchingMode: Bool = false - var networkSearchInProgress: Bool = false - var layoutForView: NCDBLayoutForView? - var searchDataSourceTask: URLSessionTask? - var providers: [NKSearchProvider]? - var searchResults: [NKSearchResult]? - var listLayout = NCListLayout() - var gridLayout = NCGridLayout() - var mediaLayout = NCMediaLayout() - var layoutType = NCGlobal.shared.layoutList - var literalSearch: String? - var tabBarSelect: NCCollectionViewCommonSelectTabBar? - var attributesZoomIn: UIMenuElement.Attributes = [] - var attributesZoomOut: UIMenuElement.Attributes = [] - var tipViewAccounts: EasyTipView? - var syncMetadatasTask: Task? + internal let database = NCManageDatabase.shared + internal let global = NCGlobal.shared + internal let utility = NCUtility() + internal let utilityFileSystem = NCUtilityFileSystem() + internal let imageCache = NCImageCache.shared + internal var dataSource = NCCollectionViewDataSource() + internal let networking = NCNetworking.shared + internal let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! + internal var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() + private var isNavigatingMetadata = false + + internal var autoUploadFileName = "" + internal var autoUploadDirectory = "" + internal let refreshControl = UIRefreshControl() + internal var searchController: UISearchController? + internal var backgroundImageView = UIImageView() + internal var serverUrl: String = "" + internal var isEditMode = false + internal var isDirectoryE2EE = false + internal var fileSelect: [String] = [] + internal var metadataFolder: tableMetadata? + internal var richWorkspaceText: String? + internal var sectionFirstHeader: NCSectionFirstHeader? + internal var sectionFirstHeaderEmptyData: NCSectionFirstHeaderEmptyData? + + // Layout + // + internal var layoutForView: NCDBLayoutForView? + internal var layoutForViewLayoutStore: String? + internal var listLayout = NCListLayout() + internal var gridLayout = NCGridLayout() + internal var mediaLayout = NCMediaLayout() + internal var layoutType = NCGlobal.shared.layoutList + + internal var tabBarSelect: NCCollectionViewCommonSelectTabBar? + + internal var attributesZoomIn: UIMenuElement.Attributes = [] + internal var attributesZoomOut: UIMenuElement.Attributes = [] + + internal var tipViewAccounts: EasyTipView? + internal var syncMetadatasTask: Task? + + // Edit Menu + // + internal let dragDropMenuIdentifier = "dragdrop" + internal var editMenuInteraction: UIEditMenuInteraction? + internal var currentMenuObjectId: String? + internal var currentMenuPoint: CGPoint = .zero + + // Search + // + internal var isSearchingMode: Bool = false + internal var networkSearchInProgress: Bool = false + internal var searchOperationHandle = NKOperationHandle() + internal var searchTask: URLSessionTask? + internal var searchResultText: String? + internal var searchResultStore: String? // DECLARE - var layoutKey = "" - var titleCurrentFolder = "" - var titlePreviusFolder: String? - var enableSearchBar: Bool = false + // + internal var layoutKey = "" + internal var titleCurrentFolder = "" + internal var titlePreviusFolder: String? + internal var enableSearchBar: Bool = false + internal var headerRichWorkspaceDisable: Bool = false + + internal var emptyImageName: String? + internal var emptyImageColors: [UIColor]? + internal var emptyTitle: String = "" + + let maxImageGrid: CGFloat = 7 + var headerMenu: NCSectionHeaderMenu? + var headerMenuTransferView = false + var headerMenuButtonsView: Bool = true var headerRichWorkspaceDisable: Bool = false + + var groupByField = "name" - var emptyImageName: String? - var emptyImageColors: [UIColor]? - var emptyTitle: String = "" + var emptyImage: UIImage? - var emptyDescription: String = "" - var emptyDataPortaitOffset: CGFloat = 0 - var emptyDataLandscapeOffset: CGFloat = -20 + internal var emptyDescription: String = "" + internal var emptyDataPortaitOffset: CGFloat = 0 + internal var emptyDataLandscapeOffset: CGFloat = -20 - var lastScale: CGFloat = 1.0 - var currentScale: CGFloat = 1.0 - var maxColumns: Int { + internal var lastScale: CGFloat = 1.0 + internal var currentScale: CGFloat = 1.0 + internal var maxColumns: Int { let screenWidth = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) let column = Int(screenWidth / 44) return column } - var transitionColumns = false - var numberOfColumns: Int = 0 - var lastNumberOfColumns: Int = 0 - + internal var transitionColumns = false + internal var numberOfColumns: Int = 0 + internal var lastNumberOfColumns: Int = 0 + + var isTransitioning: Bool = false + var selectableDataSource: [RealmSwiftObject] { dataSource.getMetadataSourceForAllSections() } + var pushed: Bool = false + var emptyDataSet: NCEmptyDataSet? + let heightHeaderRecommendations: CGFloat = 160 let heightHeaderSection: CGFloat = 30 - @MainActor - var session: NCSession.Session { - NCSession.shared.getSession(controller: tabBarController) - } - - var isLayoutPhoto: Bool { + internal var isLayoutPhoto: Bool { layoutForView?.layout == global.layoutPhotoRatio || layoutForView?.layout == global.layoutPhotoSquare } - var isLayoutGrid: Bool { + internal var isLayoutGrid: Bool { layoutForView?.layout == global.layoutGrid } - var isLayoutList: Bool { + internal var isLayoutList: Bool { layoutForView?.layout == global.layoutList } - var showDescription: Bool { + internal var showDescription: Bool { !headerRichWorkspaceDisable && NCPreferences().showDescription } - var isRecommendationActived: Bool { + internal var isRecommendationActived: Bool { let capabilities = NCNetworking.shared.capabilities[session.account] ?? NKCapabilities.Capabilities() return self.serverUrl == self.utilityFileSystem.getHomeServer(session: self.session) && capabilities.recommendations } - var infoLabelsSeparator: String { + internal var infoLabelsSeparator: String { layoutForView?.layout == global.layoutList ? " - " : "" } @MainActor - var controller: NCMainTabBarController? { + internal var session: NCSession.Session { + NCSession.shared.getSession(controller: tabBarController) + } + + @MainActor + internal var controller: NCMainTabBarController? { self.tabBarController as? NCMainTabBarController } - var mainNavigationController: NCMainNavigationController? { + @MainActor + internal var mainNavigationController: NCMainNavigationController? { self.navigationController as? NCMainNavigationController } - var sceneIdentifier: String { + @MainActor + internal var sceneIdentifier: String { (self.tabBarController as? NCMainTabBarController)?.sceneIdentifier ?? "" } - var isNumberOfItemsInAllSectionsNull: Bool { + @MainActor + internal var windowScene: UIWindowScene? { + SceneManager.shared.getWindowScene(controller: self.tabBarController as? NCMainTabBarController) + } + + internal var isNumberOfItemsInAllSectionsNull: Bool { var totalItems = 0 for section in 0.. Bool{ + func isApplicationUpdated() -> Bool { let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") return currentVersion != appVersion } - func redirectToPrivacyViewController(){ + func redirectToPrivacyViewController() { let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController newViewController.modalPresentationStyle = .fullScreen self.present(newViewController, animated: true, completion: nil) } - func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { - let viewController = presentationController.presentedViewController - - if viewController is NCViewerRichWorkspaceWebView { - closeRichWorkspaceWebView() - } - } - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) coordinator.animate(alongsideTransition: { _ in - let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) { - self.collectionView?.collectionViewLayout.invalidateLayout() - } - animator.startAnimation() + self.collectionView?.collectionViewLayout.invalidateLayout() }) self.dismissTip() @@ -387,31 +485,19 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, override var canBecomeFirstResponder: Bool { return true } - - @objc func updateIcons() { -// collectionView.reloadData() - Task { - await self.reloadDataSource() - } - } - func isApplicationUpdated() -> Bool { - let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" - let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") - return currentVersion != appVersion - } - - func redirectToPrivacyViewController() { - let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) - let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as? UINavigationController - newViewController?.modalPresentationStyle = .fullScreen - self.present(newViewController!, animated: true, completion: nil) + func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { + let viewController = presentationController.presentedViewController + + if viewController is NCViewerRichWorkspaceWebView { + closeRichWorkspaceWebView() + } } // MARK: - NotificationCenter @objc func applicationWillResignActive(_ notification: NSNotification) { - mainNavigationController?.resetPlusButtonAlpha() + self.mainNavigationController?.menuPlus?.resetPlusButtonAlpha() } @objc func closeRichWorkspaceWebView() { @@ -426,55 +512,396 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 - func changeLayout(withSubFolders: Bool) { - if self.layoutForView?.layout == layoutForView.layout { - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + if serverUrl == homeServer || numFoldersLayoutsForView == 1 { + Task { + await setLayout(layoutForView: layoutForView) + await self.reloadDataSource() + } + } else { + let alertController = UIAlertController(title: NSLocalizedString("_propagate_layout_", comment: ""), message: nil, preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default, handler: { _ in Task { + await self.setLayout(layoutForView: layoutForView, withSubFolders: true) await self.reloadDataSource() } + })) + alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in + Task { + await self.setLayout(layoutForView: layoutForView) + await self.reloadDataSource() + } + })) + + self.present(alertController, animated: true) + } + self.collectionView.collectionViewLayout.invalidateLayout() + +// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() + } + + internal func setLayout(layoutForView: NCDBLayoutForView, withSubFolders: Bool = false) async { + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + layoutForView.layout = layoutForView.layout + self.layoutType = layoutForView.layout + + collectionView.reloadData() + + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break + } + + collectionView.collectionViewLayout.invalidateLayout() + } + + @objc func reloadDataSource(_ notification: NSNotification) { + if let userInfo = notification.userInfo as? NSDictionary { + if let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } + } + + if let clearDataSource = userInfo["clearDataSource"] as? Bool, clearDataSource { + self.dataSource.removeAll() + } + } + + reloadDataSource() + } + + @objc func getServerData(_ notification: NSNotification) { + if let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { return } + } + + getServerData() + } - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) - layoutForView.layout = layoutForView.layout - self.layoutType = layoutForView.layout + @objc func reloadHeader(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + account == session.account + else { return } - collectionView.reloadData() + self.collectionView.reloadData() + } - switch layoutForView.layout { - case global.layoutList: - self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) - case global.layoutGrid: - self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) - case global.layoutPhotoSquare, global.layoutPhotoRatio: - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) - default: - break + @objc func changeStatusFolderE2EE(_ notification: NSNotification) { + reloadDataSource() + } + + @objc func closeRichWorkspaceWebView() { + reloadDataSource() + } + + @objc func deleteFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError else { return } + + if error == .success { + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } } + } else { + NCContentPresenter().showError(error: error) + } - self.collectionView.collectionViewLayout.invalidateLayout() + reloadDataSource() + } - Task { - await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + @objc func copyMoveFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + account == session.account else { return } + + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) } } - if serverUrl == homeServer || numFoldersLayoutsForView == 1 { - changeLayout(withSubFolders: false) + if serverUrl == self.serverUrl { + reloadDataSource() + } + } + + @objc func renameFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let error = userInfo["error"] as? NKError, + account == session.account + else { return } + + if error == .success { + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + } + + if serverUrl == self.serverUrl { + if error != .success { + NCContentPresenter().showError(error: error) + } + reloadDataSource() } else { - let alertController = UIAlertController(title: NSLocalizedString("_propagate_layout_", comment: ""), message: nil, preferredStyle: .alert) + collectionView.reloadData() + } + } - alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default, handler: { _ in - changeLayout(withSubFolders: true) - })) - alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in - changeLayout(withSubFolders: false) - })) + @objc func createFolder(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let account = userInfo["account"] as? String, + account == session.account, + let withPush = userInfo["withPush"] as? Bool, + let metadata = database.getMetadataFromOcId(ocId) + else { return } - self.present(alertController, animated: true) + if isSearchingMode { + return networkSearch() + } + + if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { + reloadDataSource() + } else if withPush, metadata.serverUrl == self.serverUrl { + reloadDataSource() + if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { + if sceneIdentifier == controller?.sceneIdentifier { + pushMetadata(metadata) + } + } else { + pushMetadata(metadata) + } + } + } + + @objc func favoriteFile(_ notification: NSNotification) { + if isSearchingMode { + return networkSearch() + } + + if self is NCFavorite { + return reloadDataSource() + } + + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + serverUrl == self.serverUrl + else { return } + + reloadDataSource() + } + + @objc func downloadStartFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func downloadedFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func downloadCancelFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadStartFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + !isSearchingMode, + let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) + else { return } + + // Header view trasfer + if metadata.isTransferInForeground { + NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) + DispatchQueue.main.async { self.collectionView?.reloadData() } + } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadedFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadedLivePhoto(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadCancelFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func updateShare(_ notification: NSNotification) { + if isSearchingMode { + networkSearch() + } else { + self.dataSource.removeAll() + getServerData() + } + } + + @objc func triggerProgressTask(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let progressNumber = userInfo["progress"] as? NSNumber, + let totalBytes = userInfo["totalBytes"] as? Int64, + let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, + let ocId = userInfo["ocId"] as? String, + let ocIdTransfer = userInfo["ocIdTransfer"] as? String, + let session = userInfo["session"] as? String + else { return } + + let chunk: Int = userInfo["chunk"] as? Int ?? 0 + let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false + + let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) + + // HEADER +// if self.headerMenuTransferView, transfer.session.contains("upload") { +// self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) +// self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) +// } + + DispatchQueue.main.async { + if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { + if NCNetworking.shared.transferInForegorund?.ocId == ocId { + NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue + } else { + NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) + self.collectionView.reloadData() + } + self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue + } else { + guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, + let cell = self.collectionView?.cellForItem(at: indexPath), + let cell = cell as? NCCellProtocol else { return } + if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { + cell.fileProgressView?.isHidden = true + cell.fileProgressView?.progress = .zero + cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) + if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { + cell.writeInfoDateSize(date: metadata.date, size: metadata.size) + } else { + cell.fileInfoLabel?.text = "" + cell.fileSubinfoLabel?.text = "" + } + } else { + cell.fileProgressView?.isHidden = false + cell.fileProgressView?.progress = progressNumber.floatValue + cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) + let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal + if status == NCGlobal.shared.metadataStatusDownloading { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) + } else if status == NCGlobal.shared.metadataStatusUploading { + if totalBytes > 0 { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) + } else { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" + } + } + } + } } } + + // MARK: - Layout + func setNavigationLeftItems() { + navigationItem.title = titleCurrentFolder + } + func getNavigationTitle() -> String { let tblAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) if let tblAccount, @@ -517,11 +944,44 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.navigationItem.title = self.titleCurrentFolder } + // MARK: - Empty + + func emptyDataSetView(_ view: NCEmptyView) { + + self.emptyDataSet?.setOffset(getHeaderHeight()) + if isSearchingMode { + view.emptyImage.image = UIImage(named: "search")?.image(color: .gray, size: UIScreen.main.bounds.width) + if self.dataSourceTask?.state == .running { + view.emptyTitle.text = NSLocalizedString("_search_in_progress_", comment: "") + } else { + view.emptyTitle.text = NSLocalizedString("_search_no_record_found_", comment: "") + } + view.emptyDescription.text = NSLocalizedString("_search_instruction_", comment: "") + } else if self.dataSourceTask?.state == .running { + view.emptyImage.image = UIImage(named: "networkInProgress")?.image(color: .gray, size: UIScreen.main.bounds.width) + view.emptyTitle.text = NSLocalizedString("_request_in_progress_", comment: "") + view.emptyDescription.text = "" + } else { + if serverUrl.isEmpty { + view.emptyImage.image = emptyImage + view.emptyTitle.text = NSLocalizedString(emptyTitle, comment: "") + view.emptyDescription.text = NSLocalizedString(emptyDescription, comment: "") + } else { + view.emptyImage.image = UIImage(named: "folder_nmcloud") + view.emptyTitle.text = NSLocalizedString("_files_no_files_", comment: "") + view.emptyDescription.text = NSLocalizedString("_no_file_pull_down_", comment: "") + } + } + } + // MARK: - SEARCH func searchController(enabled: Bool) { - guard enableSearchBar else { return } + guard enableSearchBar else { + return + } searchController?.searchBar.isUserInteractionEnabled = enabled + if enabled { searchController?.searchBar.alpha = 1 } else { @@ -530,25 +990,31 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } func updateSearchResults(for searchController: UISearchController) { - self.literalSearch = searchController.searchBar.text + searchResultText = searchController.searchBar.text } func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { - isSearchingMode = true - self.providers?.removeAll() - self.dataSource.removeAll() - Task { - await self.reloadDataSource() - } // TIP dismissTip() - // - mainNavigationController?.hiddenPlusButton(true) + + // (+) + self.mainNavigationController?.menuPlus?.hiddenPlusButton(true) + + if !isSearchingMode { + self.isSearchingMode = true + self.dataSource.removeAll() + self.collectionView.reloadData() + } } func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { - if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { - networkSearch() + if isSearchingMode, + searchResultText?.count ?? 0 >= 2 { + Task { + await self.search() + } +// if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { +// networkSearch() } } @@ -556,7 +1022,6 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.networking.cancelUnifiedSearchFiles() self.isSearchingMode = false - self.networkSearchInProgress = false self.literalSearch = "" self.providers?.removeAll() self.dataSource.removeAll() @@ -569,191 +1034,176 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // MARK: - TAP EVENT - func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { - tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) + func tapMoreListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) } - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { - tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) } func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) { + if isEditMode { return } guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } - TealiumHelper.shared.trackEvent(title: "magentacloud-app.filebrowser.sharing", data: ["": ""]) - appDelegate.adjust.trackEvent(TriggerEvent(Sharing.rawValue)) NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) +// NCDownloadAction.shared.openShare(viewController: self, metadata: metadata, page: .sharing) + TealiumHelper.shared.trackEvent(title: "magentacloud-app.filebrowser.sharing", data: ["": ""]) + appDelegate.adjust.trackEvent(TriggerEvent(Sharing.rawValue)) } - func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + func tapMoreGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { + if isEditMode { return } guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } - toggleMenu(metadata: metadata, image: image, sender: sender) +// toggleMenu(metadata: metadata, image: image) + if namedButtonMore == NCGlobal.shared.buttonMoreMore || namedButtonMore == NCGlobal.shared.buttonMoreLock { + toggleMenu(metadata: metadata, image: image) + } else if namedButtonMore == NCGlobal.shared.buttonMoreStop { + Task { + await cancelSession(metadata: metadata) + } + } } -// func tapRichWorkspace(_ sender: Any) { -// if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { -// if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { -// viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" -// viewerRichWorkspace.serverUrl = serverUrl -// viewerRichWorkspace.delegate = self -// -// navigationController.modalPresentationStyle = .fullScreen -// self.present(navigationController, animated: true, completion: nil) -// } -// } -// } - -// func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { -// toggleMenu(metadata: metadata, image: image, sender: sender) -// } - -// func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { -// unifiedSearchMore(metadataForSection: metadataForSection) -// } - -// func tapRecommendations(with metadata: tableMetadata) { -// didSelectMetadata(metadata, withOcIds: false) -// } - - func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func tapRichWorkspace(_ sender: Any) { + if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { + if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { + viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" + viewerRichWorkspace.serverUrl = serverUrl + viewerRichWorkspace.delegate = self - func longPressGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + navigationController.modalPresentationStyle = .fullScreen + self.present(navigationController, animated: true, completion: nil) + } + } + } - func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + // (+) + self.mainNavigationController?.menuPlus?.hiddenPlusButton(false) - func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + self.isSearchingMode = false + self.networkSearchInProgress = false + self.searchResultText = nil + self.searchResultStore = nil - func longPressMoreGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + Task { + await searchOperationHandle.cancel() + await reloadDataSource() + + // Restore Layout + if let layoutForViewLayoutStore { + let layoutForView = database.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) + layoutForView.layout = layoutForViewLayoutStore + await setLayout(layoutForView: layoutForView) + } + layoutForViewLayoutStore = nil - @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { - openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) + // update Option menu + await mainNavigationController?.updateMenuOption() + } } - func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { - return UIContextMenuConfiguration(identifier: nil, previewProvider: { - return nil - }, actionProvider: { _ in - return nil - }) + @MainActor + func setSearchBarLoading(_ loading: Bool) { + guard let textField = searchController?.searchBar.searchTextField else { + return + } + if loading { + let spinner = UIActivityIndicatorView(style: .medium) + spinner.startAnimating() + textField.rightView = spinner + textField.rightViewMode = .always + } else { + textField.rightView = nil + } } - func openMenuItems(with objectId: String?, gestureRecognizer: UILongPressGestureRecognizer) { - if gestureRecognizer.state != .began { return } + // MARK: - TAP EVENT - var listMenuItems: [UIMenuItem] = [] - let touchPoint = gestureRecognizer.location(in: collectionView) + func tapRecommendations(with metadata: tableMetadata) { + didSelectMetadata(metadata, withOcIds: false) + } - becomeFirstResponder() + func tapButtonSwitch(_ sender: Any) { + guard !isTransitioning else { return } + isTransitioning = true - if !serverUrl.isEmpty { - listMenuItems.append(UIMenuItem(title: NSLocalizedString("_paste_file_", comment: ""), action: #selector(pasteFilesMenu(_:)))) - } + guard let layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) else { return } - if !listMenuItems.isEmpty { - UIMenuController.shared.menuItems = listMenuItems - UIMenuController.shared.showMenu(from: collectionView, rect: CGRect(x: touchPoint.x, y: touchPoint.y, width: 0, height: 0)) + if layoutForView.layout == NCGlobal.shared.layoutGrid { + layoutForView.layout = NCGlobal.shared.layoutList + } else { + layoutForView.layout = NCGlobal.shared.layoutGrid } + self.layoutForView = NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) + self.collectionView.reloadData() + self.collectionView.collectionViewLayout.invalidateLayout() + self.collectionView.setCollectionViewLayout(layoutForView.layout == NCGlobal.shared.layoutList ? self.listLayout : self.gridLayout, animated: true) {_ in self.isTransitioning = false } } - // MARK: - Menu Item - - override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - if #selector(pasteFilesMenu(_:)) == action { - if !UIPasteboard.general.items.isEmpty, !(metadataFolder?.e2eEncrypted ?? false) { - return true - } - } else if #selector(copyMenuFile(_:)) == action { - return true - } else if #selector(moveMenuFile(_:)) == action { - return true - } - - return false + func tapButtonOrder(_ sender: Any) { + +// if let titleButtonHeader = NCKeychain().getTitleButtonHeader(account: session.account), !titleButtonHeader.isEmpty { +// layoutForView?.titleButtonHeader = titleButtonHeader +// } +// NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView!) + + let sortMenu = NCSortMenu() + sortMenu.toggleMenu(viewController: self, account: session.account, key: layoutKey, sortButton: sender as? UIButton, serverUrl: serverUrl) } - @objc func pasteFilesMenu(_ sender: Any?) { - Task {@MainActor in - guard let tblAccount = await NCManageDatabase.shared.getTableAccountAsync(account: session.account) else { - return - } - let scene = SceneManager.shared.getWindow(controller: controller)?.windowScene - let token = showHudBanner( - scene: scene, - title: NSLocalizedString("_upload_in_progress_", comment: "")) - - for (index, items) in UIPasteboard.general.items.enumerated() { - for item in items { - let capabilities = await NKCapabilities.shared.getCapabilities(for: session.account) - let results = NKFilePropertyResolver().resolve(inUTI: item.key, capabilities: capabilities) - guard let data = UIPasteboard.general.data(forPasteboardType: item.key, - inItemSet: IndexSet([index]))?.first - else { - continue - } - let fileName = results.name + "_" + NCPreferences().incrementalNumber + "." + results.ext - let serverUrlFileName = utilityFileSystem.createServerUrl(serverUrl: serverUrl, fileName: fileName) - let ocIdUpload = UUID().uuidString - let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId( - ocIdUpload, - fileName: fileName, - userId: tblAccount.userId, - urlBase: tblAccount.urlBase - ) - do { - try data.write(to: URL(fileURLWithPath: fileNameLocalPath)) - } catch { - continue - } + func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func longPressListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func tapShareListItem(with objectId: String, indexPath: IndexPath, sender: Any) { } - let resultsUpload = await NCNetworking.shared.uploadFile(account: session.account, - fileNameLocalPath: fileNameLocalPath, - serverUrlFileName: serverUrlFileName) { _ in - } progressHandler: { _, _, fractionCompleted in - Task {@MainActor in - LucidBanner.shared.update( - payload: LucidBannerPayload.Update(progress: fractionCompleted), - for: token - ) - } - } + func longPressMoreListItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func longPressGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func longPressMoreGridItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func tapButtonTransfer(_ sender: Any) { } + + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } + + @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { + openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) + } - if resultsUpload.error == .success, - let etag = resultsUpload.etag, - let ocId = resultsUpload.ocId { - let toPath = self.utilityFileSystem.getDirectoryProviderStorageOcId( - ocId, - fileName: fileName, - userId: tblAccount.userId, - urlBase: tblAccount.urlBase) - self.utilityFileSystem.moveFile(atPath: fileNameLocalPath, toPath: toPath) - NCManageDatabase.shared.addLocalFile( - account: session.account, - etag: etag, - ocId: ocId, - fileName: fileName) - Task { - await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadDataSource(serverUrl: self.serverUrl, requestData: true, status: nil) - } - } - } else { - Task { - await showErrorBanner(scene: scene, text: resultsUpload.error.errorDescription) - } - } - } - } - LucidBanner.shared.dismiss() - } + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { + return UIContextMenuConfiguration(identifier: nil, previewProvider: { + return nil + }, actionProvider: { _ in + return nil + }) } // MARK: - DataSource - @MainActor - func reloadDataSource() async { - if !isSearchingMode { - Task.detached { - if await self.isRecommendationActived() { + @objc func reloadDataSource() async { + + // get auto upload folder + autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() + autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: session.urlBase, userId: session.userId, account: session.account) + + // get layout for view + layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) + // set GroupField for Grid + if !isSearchingMode && layoutForView?.layout == NCGlobal.shared.layoutGrid { + groupByField = "classFile" + } else { + groupByField = "name" + } + + if isSearchingMode { + isDirectoryEncrypted = false + } else { + isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) + if isRecommendationActived { + Task.detached { await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) } } @@ -763,179 +1213,135 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, delegate.transferReloadData(serverUrl: self.serverUrl) } - await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + await mainNavigationController?.updateMenuOption() + (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + self.refreshControl.endRefreshing() + self.collectionView.reloadData() + self.setNavigationRightItems() + } } func getServerData(forced: Bool = false) async { } - @objc func networkSearch() { - guard !networkSearchInProgress else { - return - } - guard !session.account.isEmpty, - let literalSearch = literalSearch, - !literalSearch.isEmpty else { + // MARK: - Push metadata + + /// Pushes or reuses the folder view controller associated with the provided metadata. + /// + /// - Parameter metadata: The metadata representing the selected folder. + @MainActor + func pushMetadata(_ metadata: tableMetadata) async { + guard !isNavigatingMetadata, + let navigationController = self.navigationController, + let navigationCollectionViewCommon = self.controller?.navigationCollectionViewCommon else { return } - let capabilities = NCNetworking.shared.capabilities[session.account] ?? NKCapabilities.Capabilities() - self.networkSearchInProgress = true - self.dataSource.removeAll() - Task { - await self.reloadDataSource() + isNavigatingMetadata = true + defer { + isNavigatingMetadata = false } - if capabilities.serverVersionMajor >= global.nextcloudVersion20 { - self.networking.unifiedSearchFiles(literal: literalSearch, account: session.account) { task in - self.searchDataSourceTask = task - Task { - await self.reloadDataSource() - } - } providers: { account, searchProviders in - self.providers = searchProviders - self.searchResults = [] - self.dataSource = NCCollectionViewDataSource(metadatas: [], - layoutForView: self.layoutForView, - providers: self.providers, - searchResults: self.searchResults, - account: account) - } update: { _, _, searchResult, metadatas in - guard let metadatas, !metadatas.isEmpty, self.isSearchingMode, let searchResult else { return } - self.networking.unifiedSearchQueue.addOperation(NCCollectionViewUnifiedSearch(collectionViewCommon: self, metadatas: metadatas, searchResult: searchResult)) - } completion: { _, _ in - Task { - await self.reloadDataSource() - } - self.networkSearchInProgress = false - } - } else { - self.networking.searchFiles(literal: literalSearch, account: session.account) { task in - self.searchDataSourceTask = task - Task { - await self.reloadDataSource() - } - } completion: { metadatasSearch, error in - Task { - guard let metadatasSearch, - error == .success, - self.isSearchingMode - else { - self.networkSearchInProgress = false - await self.reloadDataSource() - return - } - let ocId = metadatasSearch.map { $0.ocId } - let metadatas = await self.database.getMetadatasAsync(predicate: NSPredicate(format: "ocId IN %@", ocId), - withLayout: self.layoutForView, - withAccount: self.session.account) - - self.dataSource = NCCollectionViewDataSource(metadatas: metadatas, - layoutForView: self.layoutForView, - providers: self.providers, - searchResults: self.searchResults, - account: self.session.account) - self.networkSearchInProgress = false - await self.reloadDataSource() - } - } - } - } - - func unifiedSearchMore(metadataForSection: NCMetadataForSection?) { - guard let metadataForSection = metadataForSection, let lastSearchResult = metadataForSection.lastSearchResult, let cursor = lastSearchResult.cursor, let term = literalSearch else { return } + let serverUrlPush = utilityFileSystem.createServerUrl( + serverUrl: metadata.serverUrl, + fileName: metadata.fileName + ) - metadataForSection.unifiedSearchInProgress = true - Task { - await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: nil) - } + // Update the last opening date without blocking the main flow. + Task.detached(priority: .utility) { [database] in + await database.setDirectoryLastOpeningDateAsync(ocId: metadata.ocId) } - self.networking.unifiedSearchFilesProvider(id: lastSearchResult.id, term: term, limit: 5, cursor: cursor, account: session.account) { task in - self.searchDataSourceTask = task - Task { - await self.reloadDataSource() - } - } completion: { _, searchResult, metadatas, error in - if error != .success { - Task { - await showErrorBanner(controller: self.controller, text: error.errorDescription) - } - } + guard navigationController.transitionCoordinator == nil else { + return + } - metadataForSection.unifiedSearchInProgress = false - guard let searchResult = searchResult, let metadatas = metadatas else { return } - self.dataSource.appendMetadatasToSection(metadatas, metadataForSection: metadataForSection, lastSearchResult: searchResult) + if let existingEntry = navigationCollectionViewCommon.first(where: { + $0.navigationController === navigationController && $0.serverUrl == serverUrlPush + }) { + let viewController = existingEntry.viewController - Task { - await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: nil) - } + if navigationController.topViewController === viewController { + return } - } - } - // MARK: - Push metadata + if navigationController.viewControllers.contains(where: { $0 === viewController }) { + navigationController.popToViewController(viewController, animated: true) + return + } - func pushMetadata(_ metadata: tableMetadata) { - guard let navigationCollectionViewCommon = self.controller?.navigationCollectionViewCommon else { + navigationController.pushViewController(viewController, animated: true) return } - let serverUrlPush = utilityFileSystem.createServerUrl(serverUrl: metadata.serverUrl, fileName: metadata.fileName) - // Set Last Opening Date - Task { - await database.setDirectoryLastOpeningDateAsync(ocId: metadata.ocId) + guard let viewController = UIStoryboard(name: "NCFiles", bundle: nil).instantiateInitialViewController() as? NCFiles else { + return } - if let viewController = navigationCollectionViewCommon.first(where: { $0.navigationController == self.navigationController && $0.serverUrl == serverUrlPush})?.viewController, viewController.isViewLoaded { - navigationController?.pushViewController(viewController, animated: true) - } else { - if let viewController: NCFiles = UIStoryboard(name: "NCFiles", bundle: nil).instantiateInitialViewController() as? NCFiles { - viewController.serverUrl = serverUrlPush - viewController.titlePreviusFolder = navigationItem.title - viewController.titleCurrentFolder = metadata.fileNameView + viewController.serverUrl = serverUrlPush + viewController.titlePreviusFolder = navigationItem.title + viewController.titleCurrentFolder = metadata.fileNameView - navigationCollectionViewCommon.append(NavigationCollectionViewCommon(serverUrl: serverUrlPush, navigationController: self.navigationController, viewController: viewController)) + navigationCollectionViewCommon.append( + NavigationCollectionViewCommon( + serverUrl: serverUrlPush, + navigationController: navigationController, + viewController: viewController + ) + ) - navigationController?.pushViewController(viewController, animated: true) - } - } + navigationController.pushViewController(viewController, animated: true) } - // MARK: - Header size - - func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, - heightHeaderRecommendations: CGFloat, - heightHeaderSection: CGFloat) { - var heightHeaderRichWorkspace: CGFloat = 0 - var heightHeaderRecommendations: CGFloat = 0 - var heightHeaderSection: CGFloat = 0 - - if showDescription, - !isSearchingMode, - let richWorkspaceText = self.richWorkspaceText, - !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { - heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 - } + func pushViewController(viewController: UIViewController) { + if pushed { return } - if isRecommendationActived, - !isSearchingMode, - NCPreferences().showRecommendedFiles, - !self.database.getRecommendedFiles(account: self.session.account).isEmpty { - heightHeaderRecommendations = self.heightHeaderRecommendations - heightHeaderSection = self.heightHeaderSection - } + pushed = true + navigationController?.pushViewController(viewController, animated: true) + } + + // MARK: - Header size - if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { - if section == 0 { - return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) - } else { - return (0, 0, self.heightHeaderSection) - } - } else { - return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) +// func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, +// heightHeaderRecommendations: CGFloat, +// heightHeaderSection: CGFloat) { +// var heightHeaderRichWorkspace: CGFloat = 0 +// var heightHeaderRecommendations: CGFloat = 0 +// var heightHeaderSection: CGFloat = 0 +// +// if showDescription, +// !isSearchingMode, +// let richWorkspaceText = self.richWorkspaceText, +// !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { +// heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 +// } +// +// if isRecommendationActived, +// !isSearchingMode, +// NCKeychain().showRecommendedFiles, +// !self.database.getRecommendedFiles(account: self.session.account).isEmpty { +// heightHeaderRecommendations = self.heightHeaderRecommendations +// heightHeaderSection = self.heightHeaderSection +// } +// +// if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { +// if section == 0 { +// return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) +// } else { +// return (0, 0, self.heightHeaderSection) +// } +// } else { +// return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) +// } +// } + + func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { + if headerMenuTransferView, + NCNetworking.shared.isOnline, + let results = database.getResultsMetadatas(predicate: NSPredicate(format: "status IN %@", [global.metadataStatusWaitUpload, global.metadataStatusUploading])), + !results.isEmpty { + return Array(results) } + return nil } func sizeForHeaderInSection(section: Int) -> CGSize { @@ -944,7 +1350,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let isIphone = UIDevice.current.userInterfaceIdiom == .phone if self.dataSource.isEmpty() { - height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset) + height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset, isHeaderMenuTransferViewEnabled: isHeaderMenuTransferViewEnabled() != nil) } else if isEditMode || (isLandscape && isIphone) { return CGSize.zero } else { @@ -998,16 +1404,22 @@ extension NCCollectionViewCommon: NCSectionFirstHeaderDelegate { } func tapRecommendations(with metadata: tableMetadata) { - didSelectMetadata(metadata, withOcIds: false) + Task { + await didSelectMetadata(metadata, withOcIds: false) + } } } extension NCCollectionViewCommon: NCSectionFooterDelegate { func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { - unifiedSearchMore(metadataForSection: metadataForSection) + Task { + await unifiedSearchMore(metadataForSection: metadataForSection) + } } } +// MARK: - Transfer Delegate + extension NCCollectionViewCommon: NCTransferDelegate { func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } @@ -1030,28 +1442,37 @@ extension NCCollectionViewCommon: NCTransferDelegate { Task { if error != .success, error.errorCode != global.errorResourceNotFound { - await showErrorBanner(controller: self.controller, text: error.errorDescription) + await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) } + guard session.account == account else { return } - if status == self.global.networkingStatusCreateFolder { - if serverUrl == self.serverUrl, - selector != self.global.selectorUploadAutoUpload, - let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { - self.pushMetadata(metadata) + if self.isSearchingMode { + await self.debouncerNetworkSearch.call { + await self.search() } return } - if self.isSearchingMode { - await self.debouncerNetworkSearch.call { - self.networkSearch() + switch status { + case self.global.networkingStatusCreateFolder: + if error == .success, + serverUrl == self.serverUrl, + selector != self.global.selectorUploadAutoUpload, + let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { + if metadata.e2eEncrypted { + await self.reloadDataSource() + } else { + await self.pushMetadata(metadata) + } } - } else if self.serverUrl == serverUrl || destination == self.serverUrl || self.serverUrl.isEmpty { - await self.debouncerReloadDataSource.call { - await self.reloadDataSource() + default: + if self.serverUrl == serverUrl || destination == self.serverUrl || self.serverUrl.isEmpty { + await self.debouncerReloadDataSource.call { + await self.reloadDataSource() + } } } } @@ -1061,7 +1482,7 @@ extension NCCollectionViewCommon: NCTransferDelegate { Task { if self.isSearchingMode { await self.debouncerNetworkSearch.call { - self.networkSearch() + await self.search() } return } From 4fa07ebfbfc7ebfbd165eb6b527bce6870d3f2bd Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Tue, 30 Sep 2025 22:00:01 +0530 Subject: [PATCH 15/40] NMC 1984 - Privacy policy customisation added --- .../NCCollectionViewCommon.swift | 666 +++++++++++++----- 1 file changed, 498 insertions(+), 168 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 5ce5eb5132..2e9fbed9f5 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -4,74 +4,64 @@ import UIKit import SwiftUI +import Realm import RealmSwift import NextcloudKit import EasyTipView import LucidBanner +import MoEngageInApps -class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { -//class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionHeaderMenuDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate, NCEmptyDataSetDelegate { +class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionHeaderMenuDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate, NCEmptyDataSetDelegate { @IBOutlet weak var collectionView: UICollectionView! - internal let database = NCManageDatabase.shared - internal let global = NCGlobal.shared - internal let utility = NCUtility() - internal let utilityFileSystem = NCUtilityFileSystem() - internal let imageCache = NCImageCache.shared - internal var dataSource = NCCollectionViewDataSource() - internal let networking = NCNetworking.shared - internal let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! - internal var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() - private var isNavigatingMetadata = false - - internal var autoUploadFileName = "" - internal var autoUploadDirectory = "" - internal let refreshControl = UIRefreshControl() - internal var searchController: UISearchController? - internal var backgroundImageView = UIImageView() - internal var serverUrl: String = "" - internal var isEditMode = false - internal var isDirectoryE2EE = false - internal var fileSelect: [String] = [] - internal var metadataFolder: tableMetadata? - internal var richWorkspaceText: String? - internal var sectionFirstHeader: NCSectionFirstHeader? - internal var sectionFirstHeaderEmptyData: NCSectionFirstHeaderEmptyData? - - // Layout - // - internal var layoutForView: NCDBLayoutForView? - internal var layoutForViewLayoutStore: String? - internal var listLayout = NCListLayout() - internal var gridLayout = NCGridLayout() - internal var mediaLayout = NCMediaLayout() - internal var layoutType = NCGlobal.shared.layoutList - - internal var tabBarSelect: NCCollectionViewCommonSelectTabBar? - - internal var attributesZoomIn: UIMenuElement.Attributes = [] - internal var attributesZoomOut: UIMenuElement.Attributes = [] - - internal var tipViewAccounts: EasyTipView? - internal var syncMetadatasTask: Task? - - // Edit Menu - // - internal let dragDropMenuIdentifier = "dragdrop" - internal var editMenuInteraction: UIEditMenuInteraction? - internal var currentMenuObjectId: String? - internal var currentMenuPoint: CGPoint = .zero - - // Search - // - internal var isSearchingMode: Bool = false - internal var networkSearchInProgress: Bool = false - internal var searchOperationHandle = NKOperationHandle() - internal var searchTask: URLSessionTask? - internal var searchResultText: String? - internal var searchResultStore: String? + let database = NCManageDatabase.shared + let global = NCGlobal.shared + let utility = NCUtility() + let utilityFileSystem = NCUtilityFileSystem() + let imageCache = NCImageCache.shared + var dataSource = NCCollectionViewDataSource() + let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! + var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() + + var autoUploadFileName = "" + var autoUploadDirectory = "" + let refreshControl = UIRefreshControl() + var searchController: UISearchController? + var backgroundImageView = UIImageView() + var serverUrl: String = "" + var isEditMode = false + var isDirectoryEncrypted = false + var fileSelect: [String] = [] + var metadataFolder: tableMetadata? + var richWorkspaceText: String? + var sectionFirstHeader: NCSectionFirstHeader? + var sectionFirstHeaderEmptyData: NCSectionFirstHeaderEmptyData? + var isSearchingMode: Bool = false + var networkSearchInProgress: Bool = false + var layoutForView: NCDBLayoutForView? + var dataSourceTask: URLSessionTask? + var providers: [NKSearchProvider]? + var searchResults: [NKSearchResult]? + var listLayout = NCListLayout() + var gridLayout = NCGridLayout() + var mediaLayout = NCMediaLayout() + var layoutType = NCGlobal.shared.layoutList + var literalSearch: String? + var tabBarSelect: NCCollectionViewCommonSelectTabBar? + var attributesZoomIn: UIMenuElement.Attributes = [] + var attributesZoomOut: UIMenuElement.Attributes = [] + var syncMetadatasTask: Task? + let maxImageGrid: CGFloat = 7 +// var headerMenu: NCSectionFirstHeader? + var tipViewAccounts: EasyTipView? + var tipViewAutoUpload: EasyTipView? + var headerMenu: NCSectionHeaderMenu? + var headerMenuTransferView = false + var headerMenuButtonsView: Bool = true + var headerRichWorkspaceDisable: Bool = false + // DECLARE // internal var layoutKey = "" @@ -118,6 +108,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let heightHeaderRecommendations: CGFloat = 160 let heightHeaderSection: CGFloat = 30 + var session: NCSession.Session { + NCSession.shared.getSession(controller: tabBarController) + } + internal var isLayoutPhoto: Bool { layoutForView?.layout == global.layoutPhotoRatio || layoutForView?.layout == global.layoutPhotoSquare } @@ -163,6 +157,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, (self.tabBarController as? NCMainTabBarController)?.sceneIdentifier ?? "" } + var defaultPredicate: NSPredicate { + let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, NCGlobal.shared.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) + return predicate + } + + var personalFilesOnlyPredicate: NSPredicate { + let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND (ownerId == %@ || ownerId == '') AND mountType == '' AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, session.userId, global.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) + return predicate + } + @MainActor internal var windowScene: UIWindowScene? { SceneManager.shared.getWindowScene(controller: self.tabBarController as? NCMainTabBarController) @@ -203,6 +207,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, override func viewDidLoad() { super.viewDidLoad() + tabBarSelect = NCCollectionViewCommonSelectTabBar(controller: self.controller, delegate: self) self.navigationController?.presentationController?.delegate = self collectionView.alwaysBounceVertical = true collectionView.accessibilityIdentifier = "NCCollectionViewCommon" @@ -260,6 +265,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // Wait 1.5 seconds before resetting the button alpha try? await Task.sleep(for: .seconds(1.5)) self.mainNavigationController?.menuPlus?.resetPlusButtonAlpha() +// self.dataSource.removeAll() +// self.getServerData() +// if self.isRecommendationActived { +// Task.detached { +// await NCNetworking.shared.createRecommendations(session: self.session) +// } +// } +// self.refreshControl.endRefreshing() +// DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { +// self.resetPlusButtonAlpha() } } @@ -310,6 +325,12 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, await self.debouncerReloadData.resume() } } + + NotificationCenter.default.addObserver(self, selector: #selector(changeTheming(_:)), name: NSNotification.Name(rawValue: global.notificationCenterChangeTheming), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(reloadDataSource(_:)), name: NSNotification.Name(rawValue: global.notificationCenterReloadDataSource), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(getServerData(_:)), name: NSNotification.Name(rawValue: global.notificationCenterGetServerData), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(reloadHeader(_:)), name: NSNotification.Name(rawValue: global.notificationCenterReloadHeader), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(updateIcons), name: NSNotification.Name(rawValue: global.notificationCenterUpdateIcons), object: nil) DispatchQueue.main.async { self.collectionView?.collectionViewLayout.invalidateLayout() @@ -337,6 +358,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + AnalyticsHelper.shared.displayInAppNotification() + if titlePreviusFolder != nil { navigationController?.navigationBar.topItem?.title = titlePreviusFolder } @@ -346,11 +369,12 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, navigationController?.setNavigationBarHidden(false, animated: true) appDelegate.activeViewController = self + appDelegate.account = session.account + appDelegate.urlBase = session.urlBase + appDelegate.userId = session.userId + appDelegate.user = session.user - if tabBarSelect == nil { - tabBarSelect = NCCollectionViewCommonSelectTabBar(controller: self.controller, viewController: self, delegate: self) - } - + NCKeychain().setAccountName(account: session.account) isEditMode = false Task { @@ -385,6 +409,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + NCNetworking.shared.transferDelegate = self + NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive(_:)), name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(closeRichWorkspaceWebView), name: NSNotification.Name(rawValue: global.notificationCenterCloseRichWorkspaceWebView), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(changeStatusFolderE2EE(_:)), name: NSNotification.Name(rawValue: global.notificationCenterChangeStatusFolderE2EE), object: nil) @@ -415,6 +441,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + + NCNetworking.shared.cancelUnifiedSearchFiles() dismissTip() pushed = false toggleSelect(isOn: false) @@ -423,14 +451,14 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, Task { await searchOperationHandle.cancel() } + NCNetworking.shared.unifiedSearchQueue.cancelAll() + dataSourceTask?.cancel() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - Task { - await NCNetworking.shared.transferDispatcher.removeDelegate(self) - } + NCNetworking.shared.transferDelegate = nil NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCloseRichWorkspaceWebView), object: nil) @@ -445,6 +473,11 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCreateFolder), object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterFavoriteFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterDeleteFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterMoveFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterCopyFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterCreateFolder), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterFavoriteFile), object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDownloadStartFile), object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDownloadedFile), object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDownloadCancelFile), object: nil) @@ -456,7 +489,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterUpdateShare), object: nil) - removeImageCache(metadatas: self.dataSource.getMetadatas()) + dataSource.removeImageCache() } func isApplicationUpdated() -> Bool { @@ -488,29 +521,153 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { let viewController = presentationController.presentedViewController - + if viewController is NCViewerRichWorkspaceWebView { closeRichWorkspaceWebView() } } + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + + if let frame = tabBarController?.tabBar.frame { + tabBarSelect?.hostingController?.view.frame = frame + } + } + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + + func transferChange(status: String, + account: String, + fileName: String, + serverUrl: String, + selector: String?, + ocId: String, + destination: String?, + error: NKError) { + Task { + if error != .success, + error.errorCode != global.errorResourceNotFound { + await showErrorBanner(controller: self.controller, + errorDescription: error.errorDescription, + errorCode: error.errorCode) + } + guard session.account == account else { + return + } + + await self.debouncer.call { + switch status { + // UPLOADED, UPLOADED LIVEPHOTO, DELETE + case self.global.networkingStatusUploaded, + self.global.networkingStatusDelete, + self.global.networkingStatusCopyMove: + if self.isSearchingMode { + self.networkSearch() + } else if self.serverUrl == serverUrl || destination == self.serverUrl { + await self.reloadDataSource() + } + // DOWNLOAD + case self.global.networkingStatusDownloaded: + if serverUrl == self.serverUrl || self.serverUrl.isEmpty { + await self.reloadDataSource() + } + case self.global.networkingStatusDownloadCancel: + if serverUrl == self.serverUrl { + await self.reloadDataSource() + } + // CREATE FOLDER + case self.global.networkingStatusCreateFolder: + if serverUrl == self.serverUrl, + selector != self.global.selectorUploadAutoUpload, + let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { + self.pushMetadata(metadata) + } + // RENAME + case self.global.networkingStatusRename: + if self.isSearchingMode { + self.networkSearch() + } else if self.serverUrl == serverUrl { + await self.reloadDataSource() + } + // FAVORITE + case self.global.networkingStatusFavorite: + if self.isSearchingMode { + self.networkSearch() + } else if self is NCFavorite { + await self.reloadDataSource() + } else if self.serverUrl == serverUrl { + await self.reloadDataSource() + } + default: + break + } + } + } + } + + func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) { + Task { + await self.debouncer.call { + if requestData { + if self.isSearchingMode { + self.networkSearch() + } else if ( self.serverUrl == serverUrl) || serverUrl == nil { + Task { + await self.getServerData() + } + } + } else { + if self.isSearchingMode { + guard status != self.global.metadataStatusWaitDelete, + status != self.global.metadataStatusWaitRename, + status != self.global.metadataStatusWaitMove, + status != self.global.metadataStatusWaitCopy, + status != self.global.metadataStatusWaitFavorite else { + return + } + self.networkSearch() + } else if ( self.serverUrl == serverUrl) || serverUrl == nil { + Task { + await self.reloadDataSource() + } + } + } + } + } + } + // MARK: - NotificationCenter @objc func applicationWillResignActive(_ notification: NSNotification) { self.mainNavigationController?.menuPlus?.resetPlusButtonAlpha() +// self.resetPlusButtonAlpha() +// self.refreshControl.endRefreshing() } - @objc func closeRichWorkspaceWebView() { - Task { - await self.reloadDataSource() + @objc func reloadAvatar(_ notification: NSNotification) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.showTipAccounts() } + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError, + error.errorCode != global.errorNotModified else { return } + /// Magentacloud branding changes hide user account button on left navigation bar + setNavigationLeftItems() } - // MARK: - Layout + @objc func changeTheming(_ notification: NSNotification) { + self.reloadDataSource() + } - func changeLayout(layoutForView: NCDBLayoutForView) { - let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) - let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 + @objc func changeLayout(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let layoutForView = userInfo["layoutForView"] as? NCDBLayoutForView, + account == session.account, + serverUrl == self.serverUrl + else { return } if serverUrl == homeServer || numFoldersLayoutsForView == 1 { Task { @@ -532,9 +689,29 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, await self.reloadDataSource() } })) +// if self.layoutForView?.layout == layoutForView.layout { +// self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) +// self.reloadDataSource() +// return +// } +// +// self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) +// layoutForView.layout = layoutForView.layout +// self.layoutType = layoutForView.layout +// +// collectionView.reloadData() - self.present(alertController, animated: true) + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break } + self.collectionView.collectionViewLayout.invalidateLayout() // (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() @@ -820,16 +997,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, collectionView?.reloadData() } } - - @objc func updateShare(_ notification: NSNotification) { - if isSearchingMode { - networkSearch() - } else { - self.dataSource.removeAll() - getServerData() - } - } - + @objc func triggerProgressTask(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, let progressNumber = userInfo["progress"] as? NSNumber, @@ -844,14 +1012,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) - - // HEADER -// if self.headerMenuTransferView, transfer.session.contains("upload") { -// self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) -// self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) -// } - DispatchQueue.main.async { +// DispatchQueue.main.async { if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { if NCNetworking.shared.transferInForegorund?.ocId == ocId { NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue @@ -861,7 +1023,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, + guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId), let cell = self.collectionView?.cellForItem(at: indexPath), let cell = cell as? NCCellProtocol else { return } if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { @@ -893,8 +1055,22 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } } } +// } + } + + @objc func updateShare(_ notification: NSNotification) { + if isSearchingMode { + networkSearch() + } else { +// self.dataSource.removeAll() + getServerData() } } + + @objc func updateIcons() { + collectionView.reloadData() +// reloadDataSource() + } // MARK: - Layout @@ -903,14 +1079,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } func getNavigationTitle() -> String { - let tblAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) - if let tblAccount, - !tblAccount.alias.isEmpty { - return tblAccount.alias + let tableAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) + if let tableAccount, + !tableAccount.alias.isEmpty { + return tableAccount.alias } return NCBrandOptions.shared.brand } + func accountSettingsDidDismiss(tableAccount: tableAccount?, controller: NCMainTabBarController?) { } + @MainActor func startGUIGetServerData() { self.dataSource.setGetServerData(false) @@ -943,6 +1121,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.navigationItem.titleView = nil self.navigationItem.title = self.titleCurrentFolder } + + func resetPlusButtonAlpha(animated: Bool = true) { } + + func isHiddenPlusButton(_ isHidden: Bool) { } // MARK: - Empty @@ -1005,6 +1187,12 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.dataSource.removeAll() self.collectionView.reloadData() } +// isSearchingMode = true +// self.providers?.removeAll() +// self.dataSource.removeAll() +// self.reloadDataSource() +// // TIP +// dismissTip() } func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { @@ -1019,17 +1207,12 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - self.networking.cancelUnifiedSearchFiles() - + NCNetworking.shared.cancelUnifiedSearchFiles() self.isSearchingMode = false self.literalSearch = "" self.providers?.removeAll() self.dataSource.removeAll() - Task { - await self.reloadDataSource() - } - // - mainNavigationController?.hiddenPlusButton(false) + self.reloadDataSource() } // MARK: - TAP EVENT @@ -1037,6 +1220,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, func tapMoreListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) } + + func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) + } func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) @@ -1064,6 +1251,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } } } + + func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } +// toggleMenu(metadata: metadata, image: image) + Task { + await cancelSession(metadata: metadata) + } + } + + func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } func tapRichWorkspace(_ sender: Any) { if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { @@ -1081,16 +1278,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { // (+) self.mainNavigationController?.menuPlus?.hiddenPlusButton(false) - + self.isSearchingMode = false self.networkSearchInProgress = false self.searchResultText = nil self.searchResultStore = nil - + Task { await searchOperationHandle.cancel() await reloadDataSource() - + // Restore Layout if let layoutForViewLayoutStore { let layoutForView = database.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) @@ -1098,12 +1295,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, await setLayout(layoutForView: layoutForView) } layoutForViewLayoutStore = nil - + // update Option menu await mainNavigationController?.updateMenuOption() } } + func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?) { + toggleMenu(metadata: metadata, image: image) + } + @MainActor func setSearchBarLoading(_ loading: Bool) { guard let textField = searchController?.searchBar.searchTextField else { @@ -1143,32 +1344,28 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } func tapButtonOrder(_ sender: Any) { - -// if let titleButtonHeader = NCKeychain().getTitleButtonHeader(account: session.account), !titleButtonHeader.isEmpty { -// layoutForView?.titleButtonHeader = titleButtonHeader -// } -// NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView!) - let sortMenu = NCSortMenu() sortMenu.toggleMenu(viewController: self, account: session.account, key: layoutKey, sortButton: sender as? UIButton, serverUrl: serverUrl) } - func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func tapButtonTransfer(_ sender: Any) { } - func longPressListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } - func tapShareListItem(with objectId: String, indexPath: IndexPath, sender: Any) { } + func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func longPressListItem(with objectId: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) { + } + func longPressListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } func longPressMoreListItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } func longPressGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } func longPressMoreGridItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func tapButtonTransfer(_ sender: Any) { } - - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } - @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) } @@ -1181,14 +1378,130 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, }) } + func openMenuItems(with objectId: String?, gestureRecognizer: UILongPressGestureRecognizer) { + if gestureRecognizer.state != .began { return } + + var listMenuItems: [UIMenuItem] = [] + let touchPoint = gestureRecognizer.location(in: collectionView) + + becomeFirstResponder() + + if !serverUrl.isEmpty { + listMenuItems.append(UIMenuItem(title: NSLocalizedString("_paste_file_", comment: ""), action: #selector(pasteFilesMenu))) + } + + if !listMenuItems.isEmpty { + UIMenuController.shared.menuItems = listMenuItems + UIMenuController.shared.showMenu(from: collectionView, rect: CGRect(x: touchPoint.x, y: touchPoint.y, width: 0, height: 0)) + } + } + + // MARK: - Transfer Delegate + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + + func tranferChange(status: String, metadata: tableMetadata, error: NKError) { } + + // MARK: - Menu Item + + override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + + if #selector(pasteFilesMenu) == action { + if !UIPasteboard.general.items.isEmpty, !(metadataFolder?.e2eEncrypted ?? false) { + return true + } + } else if #selector(copyMenuFile) == action { + return true + } else if #selector(moveMenuFile) == action { + return true + } + + return false + } + + @objc func pasteFilesMenu(_ sender: Any?) { + Task {@MainActor in + guard let tblAccount = await NCManageDatabase.shared.getTableAccountAsync(account: session.account) else { + return + } + let scene = SceneManager.shared.getWindow(controller: controller)?.windowScene + let token = showHudBanner( + scene: scene, + title: NSLocalizedString("_upload_in_progress_", comment: "")) + + for (index, items) in UIPasteboard.general.items.enumerated() { + for item in items { + let capabilities = await NKCapabilities.shared.getCapabilities(for: session.account) + let results = NKFilePropertyResolver().resolve(inUTI: item.key, capabilities: capabilities) + guard let data = UIPasteboard.general.data(forPasteboardType: item.key, + inItemSet: IndexSet([index]))?.first + else { + continue + } + let fileName = results.name + "_" + NCPreferences().incrementalNumber + "." + results.ext + let serverUrlFileName = utilityFileSystem.createServerUrl(serverUrl: serverUrl, fileName: fileName) + let ocIdUpload = UUID().uuidString + let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId( + ocIdUpload, + fileName: fileName, + userId: tblAccount.userId, + urlBase: tblAccount.urlBase + ) + do { + try data.write(to: URL(fileURLWithPath: fileNameLocalPath)) + } catch { + continue + } + + let resultsUpload = await NCNetworking.shared.uploadFile(account: session.account, + fileNameLocalPath: fileNameLocalPath, + serverUrlFileName: serverUrlFileName) { _ in + } progressHandler: { _, _, fractionCompleted in + Task {@MainActor in + LucidBanner.shared.update(progress: fractionCompleted, for: token) + } + } + + if resultsUpload.error == .success, + let etag = resultsUpload.etag, + let ocId = resultsUpload.ocId { + let toPath = self.utilityFileSystem.getDirectoryProviderStorageOcId( + ocId, + fileName: fileName, + userId: tblAccount.userId, + urlBase: tblAccount.urlBase) + self.utilityFileSystem.moveFile(atPath: fileNameLocalPath, toPath: toPath) + NCManageDatabase.shared.addLocalFile( + account: session.account, + etag: etag, + ocId: ocId, + fileName: fileName) + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: serverUrl, requestData: true, status: nil) + } + } + } else { + Task {@MainActor in + await showErrorBanner(scene: scene, + errorDescription: resultsUpload.error.errorDescription, + errorCode: resultsUpload.error.errorCode) + } + } + } + } + LucidBanner.shared.dismiss() + } + } + // MARK: - DataSource - @objc func reloadDataSource() async { + @objc func reloadDataSource() { // get auto upload folder autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: session.urlBase, userId: session.userId, account: session.account) - + // get layout for view layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) // set GroupField for Grid @@ -1197,16 +1510,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } else { groupByField = "name" } - if isSearchingMode { isDirectoryEncrypted = false } else { isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) - if isRecommendationActived { - Task.detached { - await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) - } - } } await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in @@ -1221,7 +1528,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } } - func getServerData(forced: Bool = false) async { } + func getServerData() { + } // MARK: - Push metadata @@ -1291,38 +1599,55 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, navigationController.pushViewController(viewController, animated: true) } - + func pushViewController(viewController: UIViewController) { if pushed { return } pushed = true navigationController?.pushViewController(viewController, animated: true) } - + // MARK: - Header size -// func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, -// heightHeaderRecommendations: CGFloat, -// heightHeaderSection: CGFloat) { -// var heightHeaderRichWorkspace: CGFloat = 0 -// var heightHeaderRecommendations: CGFloat = 0 -// var heightHeaderSection: CGFloat = 0 +// func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { +// var headerRichWorkspace: CGFloat = 0 // -// if showDescription, -// !isSearchingMode, -// let richWorkspaceText = self.richWorkspaceText, -// !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { -// heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 -// } +// func getHeaderHeight() -> CGFloat { +// var size: CGFloat = 0 // -// if isRecommendationActived, -// !isSearchingMode, -// NCKeychain().showRecommendedFiles, -// !self.database.getRecommendedFiles(account: self.session.account).isEmpty { -// heightHeaderRecommendations = self.heightHeaderRecommendations -// heightHeaderSection = self.heightHeaderSection +// if isHeaderMenuTransferViewEnabled() != nil { +// if !isSearchingMode { +// size += global.heightHeaderTransfer +// } +// } +// if headerMenuButtonsView { +// size += NCGlobal.shared.heightButtonsView +// } +// return size // } // +//// func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, +//// heightHeaderRecommendations: CGFloat, +//// heightHeaderSection: CGFloat) { +//// var heightHeaderRichWorkspace: CGFloat = 0 +//// var heightHeaderRecommendations: CGFloat = 0 +//// var heightHeaderSection: CGFloat = 0 +//// +//// if showDescription, +//// !isSearchingMode, +//// let richWorkspaceText = self.richWorkspaceText, +//// !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { +//// heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 +//// } +//// +//// if isRecommendationActived, +//// !isSearchingMode, +//// NCKeychain().showRecommendedFiles, +//// !self.database.getRecommendedFiles(account: self.session.account).isEmpty { +//// heightHeaderRecommendations = self.heightHeaderRecommendations +//// heightHeaderSection = self.heightHeaderSection +//// } +// // if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { // if section == 0 { // return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) @@ -1331,6 +1656,15 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // } // } else { // return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) +//// +//// if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { +//// if section == 0 { +//// return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) +//// } else { +//// return (0, 0, self.heightHeaderSection) +//// } +//// } else { +//// return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) // } // } @@ -1354,8 +1688,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } else if isEditMode || (isLandscape && isIphone) { return CGSize.zero } else { - let (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) = getHeaderHeight(section: section) - height = heightHeaderRichWorkspace + heightHeaderRecommendations + heightHeaderSection + let (heightHeaderCommands, heightHeaderRichWorkspace, heightHeaderSection) = getHeaderHeight(section: section) + height = heightHeaderCommands + heightHeaderRichWorkspace + heightHeaderSection } return CGSize(width: collectionView.frame.width, height: height) @@ -1364,26 +1698,22 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // MARK: - Footer size func sizeForFooterInSection(section: Int) -> CGSize { - guard let controller else { - return CGSize.zero - } let sections = dataSource.numberOfSections() - let bottomAreaInsets: CGFloat = controller.tabBar.safeAreaInsets.bottom == 0 ? 34 : 0 - let height = controller.tabBar.frame.height + bottomAreaInsets - - if isEditMode { - return CGSize(width: collectionView.frame.width, height: 90 + height) - } - - if isSearchingMode { - return CGSize(width: collectionView.frame.width, height: 50) - } + let metadataForSection = self.dataSource.getMetadataForSection(section) + let isPaginated = metadataForSection?.lastSearchResult?.isPaginated ?? false + let metadatasCount: Int = metadataForSection?.lastSearchResult?.entries.count ?? 0 + var size = CGSize(width: collectionView.frame.width, height: 0) if section == sections - 1 { - return CGSize(width: collectionView.frame.width, height: height) + size.height += 85 } else { - return CGSize(width: collectionView.frame.width, height: 0) + size.height += 1 + } + + if isSearchingMode && isPaginated && metadatasCount > 0 { + size.height += 30 } + return size } func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } From bdf4598d83dcb21be49ac6d99adfc62cfff31fae Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:58:19 +0530 Subject: [PATCH 16/40] NMC 1984 - Privacy policy customisation added --- .../Collection Common/NCCollectionViewCommon.swift | 13 +++++++++++++ .../Settings/PrivacySettingsViewController.swift | 11 +---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 2e9fbed9f5..ecc5bcd219 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -504,6 +504,19 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS newViewController.modalPresentationStyle = .fullScreen self.present(newViewController, animated: true, completion: nil) } + + func isApplicationUpdated() -> Bool{ + let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" + let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") + return currentVersion != appVersion + } + + func redirectToPrivacyViewController(){ + let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) + let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController + newViewController.modalPresentationStyle = .fullScreen + self.present(newViewController, animated: true, completion: nil) + } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index 757d5532d5..fa1aacfc0e 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -13,7 +13,7 @@ import XLForm class PrivacySettingsViewController: XLFormViewController{ - @objc public var isShowSettingsButton: Bool = false + @objc public var isShowSettingsButton: Bool = false override func viewDidLoad() { super.viewDidLoad() @@ -35,13 +35,9 @@ class PrivacySettingsViewController: XLFormViewController{ tableView.reloadData() initializeForm() } - - //MARK: XLForm - func initializeForm() { - let form : XLFormDescriptor = XLFormDescriptor() as XLFormDescriptor form.rowNavigationOptions = XLFormRowNavigationOptions.stopDisableRow @@ -58,7 +54,6 @@ class PrivacySettingsViewController: XLFormViewController{ section.footerTitle = NSLocalizedString("_privacy_settings_help_text_", comment: "") form.addFormSection(section) - //custom cell section = XLFormSectionDescriptor.formSection(withTitle: "") section.footerTitle = NSLocalizedString("_required_data_collection_help_text_", comment: "") @@ -67,7 +62,6 @@ class PrivacySettingsViewController: XLFormViewController{ XLFormViewController.cellClassesForRowDescriptorTypes()["RequiredDataCollectionCustomCellType"] = RequiredDataCollectionSwitch.self - row = XLFormRowDescriptor(tag: "ButtonDestinationFolder", rowType: "RequiredDataCollectionCustomCellType", title: "") row.cellConfig["requiredDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue @@ -82,7 +76,6 @@ class PrivacySettingsViewController: XLFormViewController{ XLFormViewController.cellClassesForRowDescriptorTypes()["AnalysisDataCollectionCustomCellType"] = AnalysisDataCollectionSwitch.self - row = XLFormRowDescriptor(tag: "AnalysisDataCollectionSwitch", rowType: "AnalysisDataCollectionCustomCellType", title: "") row.cellConfig["analysisDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue @@ -97,13 +90,11 @@ class PrivacySettingsViewController: XLFormViewController{ section.addFormRow(row) - XLFormViewController.cellClassesForRowDescriptorTypes()["SaveSettingsButton"] = SaveSettingsCustomButtonCell.self section = XLFormSectionDescriptor.formSection(withTitle: "") form.addFormSection(section) - row = XLFormRowDescriptor(tag: "SaveSettingsButton", rowType: "SaveSettingsButton", title: "") row.cellConfig["backgroundColor"] = UIColor.clear From 947595f9fcf8968aec6a4c858e31073ae64132c5 Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Tue, 26 Dec 2023 13:29:37 +0530 Subject: [PATCH 17/40] NMC 1984 - Privacy setting view controller added --- iOSClient/Settings/PrivacySettingsViewController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index fa1aacfc0e..b3993620ba 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -35,9 +35,11 @@ class PrivacySettingsViewController: XLFormViewController{ tableView.reloadData() initializeForm() } - + //MARK: XLForm + func initializeForm() { + let form : XLFormDescriptor = XLFormDescriptor() as XLFormDescriptor form.rowNavigationOptions = XLFormRowNavigationOptions.stopDisableRow From 5fe5b4c81d9943f80b44e60f3038ee99add481fd Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Wed, 9 Apr 2025 14:10:39 +0530 Subject: [PATCH 18/40] NMC 1984 - Privacy policy customisation changes added --- .../NCCollectionViewCommon.swift | 468 +++++++++++++++--- 1 file changed, 406 insertions(+), 62 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index ecc5bcd219..d8ab0b3b9e 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -505,13 +505,13 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.present(newViewController, animated: true, completion: nil) } - func isApplicationUpdated() -> Bool{ + func isApplicationUpdated() -> Bool { let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") return currentVersion != appVersion } - func redirectToPrivacyViewController(){ + func redirectToPrivacyViewController() { let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController newViewController.modalPresentationStyle = .fullScreen @@ -1085,6 +1085,399 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // reloadDataSource() } + // MARK: - Layout + + func changeLayout(layoutForView: NCDBLayoutForView) { + let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) + let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 + + func changeLayout(withSubFolders: Bool) { + if self.layoutForView?.layout == layoutForView.layout { + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + Task { + await self.reloadDataSource() + } + return + } + + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + layoutForView.layout = layoutForView.layout + self.layoutType = layoutForView.layout + + collectionView.reloadData() + + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break + } + + self.collectionView.collectionViewLayout.invalidateLayout() + + Task { + await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + } + } + + if serverUrl == homeServer || numFoldersLayoutsForView == 1 { + changeLayout(withSubFolders: false) + } else { + let alertController = UIAlertController(title: NSLocalizedString("_propagate_layout_", comment: ""), message: nil, preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default, handler: { _ in + changeLayout(withSubFolders: true) + })) + alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in + changeLayout(withSubFolders: false) + })) + + self.present(alertController, animated: true) + } + self.collectionView.collectionViewLayout.invalidateLayout() + +// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() + } + + @objc func reloadDataSource(_ notification: NSNotification) { + if let userInfo = notification.userInfo as? NSDictionary { + if let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } + } + + if let clearDataSource = userInfo["clearDataSource"] as? Bool, clearDataSource { + self.dataSource.removeAll() + } + } + + reloadDataSource() + } + + @objc func getServerData(_ notification: NSNotification) { + if let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } + } + + getServerData() + } + + @objc func reloadHeader(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + account == session.account + else { return } + + self.collectionView.reloadData() + } + + @objc func changeStatusFolderE2EE(_ notification: NSNotification) { + reloadDataSource() + } + + @objc func closeRichWorkspaceWebView() { + reloadDataSource() + } + + @objc func deleteFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError else { return } + + if error == .success { + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + } else { + NCContentPresenter().showError(error: error) + } + + reloadDataSource() + } + + @objc func copyMoveFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + account == session.account else { return } + + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + + if serverUrl == self.serverUrl { + reloadDataSource() + } + } + + @objc func renameFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let error = userInfo["error"] as? NKError, + account == session.account + else { return } + + if error == .success { + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + } + + if serverUrl == self.serverUrl { + if error != .success { + NCContentPresenter().showError(error: error) + } + reloadDataSource() + } else { + collectionView.reloadData() + } + } + + @objc func createFolder(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let account = userInfo["account"] as? String, + account == session.account, + let withPush = userInfo["withPush"] as? Bool, + let metadata = database.getMetadataFromOcId(ocId) + else { return } + + if isSearchingMode { + return networkSearch() + } + + if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { + reloadDataSource() + } else if withPush, metadata.serverUrl == self.serverUrl { + reloadDataSource() + if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { + if sceneIdentifier == controller?.sceneIdentifier { + pushMetadata(metadata) + } + } else { + pushMetadata(metadata) + } + } + } + + @objc func favoriteFile(_ notification: NSNotification) { + if isSearchingMode { + return networkSearch() + } + + if self is NCFavorite { + return reloadDataSource() + } + + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + serverUrl == self.serverUrl + else { return } + + reloadDataSource() + } + + @objc func downloadStartFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func downloadedFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func downloadCancelFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadStartFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + !isSearchingMode, + let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) + else { return } + + // Header view trasfer + if metadata.isTransferInForeground { + NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) + DispatchQueue.main.async { self.collectionView?.reloadData() } + } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadedFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadedLivePhoto(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadCancelFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func updateShare(_ notification: NSNotification) { + if isSearchingMode { + networkSearch() + } else { + self.dataSource.removeAll() + getServerData() + } + } + + @objc func triggerProgressTask(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let progressNumber = userInfo["progress"] as? NSNumber, + let totalBytes = userInfo["totalBytes"] as? Int64, + let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, + let ocId = userInfo["ocId"] as? String, + let ocIdTransfer = userInfo["ocIdTransfer"] as? String, + let session = userInfo["session"] as? String + else { return } + + let chunk: Int = userInfo["chunk"] as? Int ?? 0 + let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false + + let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) + + // HEADER +// if self.headerMenuTransferView, transfer.session.contains("upload") { +// self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) +// self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) +// } + + DispatchQueue.main.async { + if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { + if NCNetworking.shared.transferInForegorund?.ocId == ocId { + NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue + } else { + NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) + self.collectionView.reloadData() + } + self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue + } else { + guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, + let cell = self.collectionView?.cellForItem(at: indexPath), + let cell = cell as? NCCellProtocol else { return } + if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { + cell.fileProgressView?.isHidden = true + cell.fileProgressView?.progress = .zero + cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) + if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { + cell.writeInfoDateSize(date: metadata.date, size: metadata.size) + } else { + cell.fileInfoLabel?.text = "" + cell.fileSubinfoLabel?.text = "" + } + } else { + cell.fileProgressView?.isHidden = false + cell.fileProgressView?.progress = progressNumber.floatValue + cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) + let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal + if status == NCGlobal.shared.metadataStatusDownloading { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) + } else if status == NCGlobal.shared.metadataStatusUploading { + if totalBytes > 0 { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) + } else { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" + } + } + } + } + } + } + // MARK: - Layout func setNavigationLeftItems() { @@ -1379,6 +1772,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS func longPressMoreGridItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func tapButtonTransfer(_ sender: Any) { } + + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } + @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) } @@ -1514,7 +1911,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // get auto upload folder autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: session.urlBase, userId: session.userId, account: session.account) - // get layout for view layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) // set GroupField for Grid @@ -1620,66 +2016,14 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS navigationController?.pushViewController(viewController, animated: true) } - // MARK: - Header size + func pushViewController(viewController: UIViewController) { + if pushed { return } -// func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { -// var headerRichWorkspace: CGFloat = 0 -// -// func getHeaderHeight() -> CGFloat { -// var size: CGFloat = 0 -// -// if isHeaderMenuTransferViewEnabled() != nil { -// if !isSearchingMode { -// size += global.heightHeaderTransfer -// } -// } -// if headerMenuButtonsView { -// size += NCGlobal.shared.heightButtonsView -// } -// return size -// } -// -//// func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, -//// heightHeaderRecommendations: CGFloat, -//// heightHeaderSection: CGFloat) { -//// var heightHeaderRichWorkspace: CGFloat = 0 -//// var heightHeaderRecommendations: CGFloat = 0 -//// var heightHeaderSection: CGFloat = 0 -//// -//// if showDescription, -//// !isSearchingMode, -//// let richWorkspaceText = self.richWorkspaceText, -//// !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { -//// heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 -//// } -//// -//// if isRecommendationActived, -//// !isSearchingMode, -//// NCKeychain().showRecommendedFiles, -//// !self.database.getRecommendedFiles(account: self.session.account).isEmpty { -//// heightHeaderRecommendations = self.heightHeaderRecommendations -//// heightHeaderSection = self.heightHeaderSection -//// } -// -// if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { -// if section == 0 { -// return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) -// } else { -// return (0, 0, self.heightHeaderSection) -// } -// } else { -// return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) -//// -//// if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { -//// if section == 0 { -//// return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) -//// } else { -//// return (0, 0, self.heightHeaderSection) -//// } -//// } else { -//// return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) -// } -// } + pushed = true + navigationController?.pushViewController(viewController, animated: true) + } + + // MARK: - Header size func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { if headerMenuTransferView, From ff869449de093ff41d4c6f79112b6aa52a562a79 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Tue, 30 Sep 2025 22:00:01 +0530 Subject: [PATCH 19/40] NMC 1984 - Privacy policy customisation added --- .../NCCollectionViewCommon.swift | 123 +++++++----------- 1 file changed, 46 insertions(+), 77 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index d8ab0b3b9e..d30bc10a00 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -498,19 +498,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return currentVersion != appVersion } - func redirectToPrivacyViewController() { - let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) - let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController - newViewController.modalPresentationStyle = .fullScreen - self.present(newViewController, animated: true, completion: nil) - } - - func isApplicationUpdated() -> Bool { - let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" - let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") - return currentVersion != appVersion - } - func redirectToPrivacyViewController() { let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController @@ -834,7 +821,17 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS if serverUrl == self.serverUrl { reloadDataSource() + } + + @objc func reloadAvatar(_ notification: NSNotification) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.showTipAccounts() } + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError, + error.errorCode != global.errorNotModified else { return } + /// Magentacloud branding changes hide user account button on left navigation bar + setNavigationLeftItems() } @objc func renameFile(_ notification: NSNotification) { @@ -1091,53 +1088,30 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 - func changeLayout(withSubFolders: Bool) { - if self.layoutForView?.layout == layoutForView.layout { - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) - Task { - await self.reloadDataSource() - } - return - } - - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) - layoutForView.layout = layoutForView.layout - self.layoutType = layoutForView.layout - - collectionView.reloadData() - - switch layoutForView.layout { - case global.layoutList: - self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) - case global.layoutGrid: - self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) - case global.layoutPhotoSquare, global.layoutPhotoRatio: - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) - default: - break - } - - self.collectionView.collectionViewLayout.invalidateLayout() - Task { - await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() - } + if self.layoutForView?.layout == layoutForView.layout { + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) + self.reloadDataSource() + return } - if serverUrl == homeServer || numFoldersLayoutsForView == 1 { - changeLayout(withSubFolders: false) - } else { - let alertController = UIAlertController(title: NSLocalizedString("_propagate_layout_", comment: ""), message: nil, preferredStyle: .alert) + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) + layoutForView.layout = layoutForView.layout + self.layoutType = layoutForView.layout - alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default, handler: { _ in - changeLayout(withSubFolders: true) - })) - alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in - changeLayout(withSubFolders: false) - })) + collectionView.reloadData() - self.present(alertController, animated: true) + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break } + self.collectionView.collectionViewLayout.invalidateLayout() // (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() @@ -1402,16 +1376,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS collectionView?.reloadData() } } - - @objc func updateShare(_ notification: NSNotification) { - if isSearchingMode { - networkSearch() - } else { - self.dataSource.removeAll() - getServerData() - } - } - + @objc func triggerProgressTask(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, let progressNumber = userInfo["progress"] as? NSNumber, @@ -1426,14 +1391,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) - - // HEADER -// if self.headerMenuTransferView, transfer.session.contains("upload") { -// self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) -// self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) -// } - DispatchQueue.main.async { +// DispatchQueue.main.async { if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { if NCNetworking.shared.transferInForegorund?.ocId == ocId { NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue @@ -1443,7 +1402,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, + guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId), let cell = self.collectionView?.cellForItem(at: indexPath), let cell = cell as? NCCellProtocol else { return } if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { @@ -1475,8 +1434,22 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } } +// } + } + + @objc func updateShare(_ notification: NSNotification) { + if isSearchingMode { + networkSearch() + } else { +// self.dataSource.removeAll() + getServerData() } } + + @objc func updateIcons() { + collectionView.reloadData() +// reloadDataSource() + } // MARK: - Layout @@ -1772,10 +1745,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS func longPressMoreGridItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func tapButtonTransfer(_ sender: Any) { } - - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } - @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) } @@ -2022,7 +1991,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS pushed = true navigationController?.pushViewController(viewController, animated: true) } - + // MARK: - Header size func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { From 8c42638d635c6abc1422610e34f19aa2f196ce3d Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Wed, 17 Dec 2025 18:29:46 +0530 Subject: [PATCH 20/40] NMC 1984 - Privacy policy customisation added --- .../Data/NCManageDatabase+LocalFile.swift | 40 +- .../Data/NCManageDatabase+Metadata.swift | 66 + ...mon+CollectionViewDelegateFlowLayout.swift | 43 - .../NCCollectionViewCommon+SyncMetadata.swift | 14 +- .../NCCollectionViewCommon.swift | 1923 +++++------------ .../NCBackgroundLocationUploadManager.swift | 19 +- iOSClient/NCGlobal.swift | 27 +- iOSClient/Networking/NCConfigServer.swift | 25 +- .../Networking/NCNetworking+Download.swift | 62 +- .../Networking/NCNetworking+LivePhoto.swift | 25 +- iOSClient/Networking/NCNetworking+Task.swift | 25 +- .../Networking/NCNetworking+Upload.swift | 21 +- .../Networking/NCNetworking+WebDAV.swift | 69 +- iOSClient/Networking/NCNetworking.swift | 198 ++ .../Networking/NCNetworkingProcess.swift | 1 - iOSClient/Networking/NCService.swift | 25 +- .../en.lproj/Localizable.strings | 114 +- 17 files changed, 1090 insertions(+), 1607 deletions(-) diff --git a/iOSClient/Data/NCManageDatabase+LocalFile.swift b/iOSClient/Data/NCManageDatabase+LocalFile.swift index 2146dec1ad..334e5fba25 100644 --- a/iOSClient/Data/NCManageDatabase+LocalFile.swift +++ b/iOSClient/Data/NCManageDatabase+LocalFile.swift @@ -29,14 +29,17 @@ extension NCManageDatabase { // MARK: - Realm Write - /// Adds or updates multiple local file entries corresponding to the given metadata array. - /// Uses async Realm read + single write transaction. Assumes `tableLocalFile` has a primary key. /// - Parameters: - /// - metadatas: Array of `tableMetadata` to map into `tableLocalFile`. - /// - offline: Optional override for the `offline` flag applied to all items. - func addLocalFilesAsync(metadatas: [tableMetadata], offline: Bool? = nil) async { - guard !metadatas.isEmpty else { - return + /// - metadata: The `tableMetadata` containing file details. + /// - offline: Optional flag to mark the file as available offline. + /// - Returns: Nothing. Realm write is performed asynchronously. + func addLocalFileAsync(metadata: tableMetadata, offline: Bool? = nil) async { + // Read (non-blocking): safely detach from Realm thread + let existing: tableLocalFile? = performRealmRead { realm in + realm.objects(tableLocalFile.self) + .filter(NSPredicate(format: "ocId == %@", metadata.ocId)) + .first + .map { tableLocalFile(value: $0) } } // Extract ocIds for efficient lookup @@ -59,20 +62,19 @@ extension NCManageDatabase { // Reuse existing object or create a new one let local = existingMap[metadata.ocId] ?? tableLocalFile() - local.account = metadata.account - local.etag = metadata.etag - local.exifDate = NSDate() - local.exifLatitude = "-1" - local.exifLongitude = "-1" - local.ocId = metadata.ocId - local.fileName = metadata.fileName + addObject.account = metadata.account + addObject.etag = metadata.etag + addObject.exifDate = NSDate() + addObject.exifLatitude = "-1" + addObject.exifLongitude = "-1" + addObject.ocId = metadata.ocId + addObject.fileName = metadata.fileName - if let offline { - local.offline = offline - } - - realm.add(local, update: .all) + if let offline { + addObject.offline = offline } + + realm.add(addObject, update: .all) } } diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index 67ed7ad2f2..5c3b840691 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -151,10 +151,37 @@ extension tableMetadata { (fileNameView as NSString).deletingPathExtension } + var isRenameable: Bool { + if !NCMetadataPermissions.canRename(self) { + return false + } + if lock { + return false + } + if !isDirectoryE2EE && e2eEncrypted { + return false + } + return true + } + + var isPrintable: Bool { + if isDocumentViewableOnly { + return false + } + if ["application/pdf", "com.adobe.pdf"].contains(contentType) || contentType.hasPrefix("text/") || classFile == NKTypeClassFile.image.rawValue { + return true + } + return false + } + var isSavebleInCameraRoll: Bool { return (classFile == NKTypeClassFile.image.rawValue && contentType != "image/svg+xml") || classFile == NKTypeClassFile.video.rawValue } + var isDocumentViewableOnly: Bool { + sharePermissionsCollaborationServices == NCPermissions().permissionReadShare && classFile == NKTypeClassFile.document.rawValue + } + var isAudioOrVideo: Bool { return classFile == NKTypeClassFile.audio.rawValue || classFile == NKTypeClassFile.video.rawValue } @@ -344,6 +371,17 @@ extension tableMetadata { return !lock || (lockOwner == user && lockOwnerType == 0) } + // Return if is sharable + func isSharable() -> Bool { + guard let capabilities = NCNetworking.shared.capabilities[account] else { + return false + } + if !capabilities.fileSharingApiEnabled || (capabilities.e2EEEnabled && isDirectoryE2EE), !e2eEncrypted { + return false + } + return !e2eEncrypted + } + /// Returns a detached (unmanaged) deep copy of the current `tableMetadata` object. /// /// - Note: Primitive properties and lists of primitive values (for example `shareType`) @@ -931,6 +969,34 @@ extension NCManageDatabase { .map { $0.detachedCopy() } } ?? [] } + + func getMediaMetadatas(predicate: NSPredicate, sorted: String? = nil, ascending: Bool = false) -> ThreadSafeArray? { + + do { + let realm = try Realm() + if let sorted { + var results: [tableMetadata] = [] + switch sorted {//NCPreferences().mediaSortDate { + case "date": + results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.date as Date) > ($1.date as Date) } + case "creationDate": + results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.creationDate as Date) > ($1.creationDate as Date) } + case "uploadDate": + results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.uploadDate as Date) > ($1.uploadDate as Date) } + default: + let results = realm.objects(tableMetadata.self).filter(predicate) + return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) + } + return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) + } else { + let results = realm.objects(tableMetadata.self).filter(predicate) + return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) + } + } catch let error as NSError { +// NextcloudKit.shared.nkCommonInstance.writeLog("Could not access database: \(error)") + } + return nil + } func getMetadatas(predicate: NSPredicate, sortedByKeyPath: String, diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift index 42bbbd6038..06795639bc 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift @@ -13,47 +13,4 @@ extension NCCollectionViewCommon: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { return sizeForFooterInSection(section: section) } - - func getHeaderHeight() -> CGFloat { - - var size: CGFloat = 0 - // transfer in progress - if headerMenuTransferView, - let metadata = NCManageDatabase.shared.getMetadataFromOcId(NCNetworking.shared.transferInForegorund?.ocId), - metadata.isTransferInForeground { - if !isSearchingMode { - size += NCGlobal.shared.heightHeaderTransfer - } - } else { - NCNetworking.shared.transferInForegorund = nil - } - - if headerMenuButtonsView { - size += NCGlobal.shared.heightButtonsView - } - - return size - } - - func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { - - var headerRichWorkspace: CGFloat = 0 - - if let richWorkspaceText = richWorkspaceText, showDescription { - let trimmed = richWorkspaceText.trimmingCharacters(in: .whitespaces) - if !trimmed.isEmpty && !isSearchingMode { - headerRichWorkspace = UIScreen.main.bounds.size.height / 6 - } - } - - if isSearchingMode || layoutForView?.layout == NCGlobal.shared.layoutGrid || dataSource.numberOfSections() > 1 { - if section == 0 { - return (getHeaderHeight(), headerRichWorkspace, NCGlobal.shared.heightSection) - } else { - return (0, 0, NCGlobal.shared.heightSection) - } - } else { - return (getHeaderHeight(), headerRichWorkspace, 0) - } - } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift index 5e0254858d..571306d2de 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift @@ -21,7 +21,7 @@ extension NCCollectionViewCommon { // If a sync task is already running, do not start a new one if let task = syncMetadatasTask, !task.isCancelled { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .info, message: "Exit: Another sync is already running. Skipping this one.", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .info, message: "Exit: Another sync is already running. Skipping this one.", consoleOnly: true) return } @@ -45,9 +45,9 @@ extension NCCollectionViewCommon { func stopSyncMetadata() async { if let task = syncMetadatasTask { if task.isCancelled { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .stop, message: "Sync Metadata for \(self.serverUrl) was already cancelled.", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .stop, message: "Sync Metadata for \(self.serverUrl) was already cancelled.", consoleOnly: true) } else { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .stop, message: "Stopping active Sync Metadata for \(self.serverUrl).", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .stop, message: "Stopping active Sync Metadata for \(self.serverUrl).", consoleOnly: true) } } @@ -74,7 +74,7 @@ extension NCCollectionViewCommon { return } let identifier = self.serverUrl + "_syncMetadata" - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .start, message: "Start Sync Metadata for \(self.serverUrl)") + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .start, message: "Start Sync Metadata for \(self.serverUrl)") // Always cancel and clear all tracked URLSessionTask on any exit path defer { @@ -85,7 +85,7 @@ extension NCCollectionViewCommon { // If a readFile for this serverUrl is already in-flight, do nothing if await networking.networkingTasks.isReading(identifier: identifier) { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .debug, message: "ReadFile for this \(self.serverUrl) is already in-flight.", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .debug, message: "ReadFile for this \(self.serverUrl) is already in-flight.", consoleOnly: true) return } @@ -151,9 +151,9 @@ extension NCCollectionViewCommon { // If this folder failed, skip it but keep processing others if resultsReadFolder.error == .success { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .network, message: "Read correctly: \(serverUrl)", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .network, message: "Read correctly: \(serverUrl)", consoleOnly: true) } else { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .error, message: "Read failed for \(serverUrl) with error: \(resultsReadFolder.error.errorDescription)") + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .error, message: "Read failed for \(serverUrl) with error: \(resultsReadFolder.error.errorDescription)") return } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index d30bc10a00..2595fb1348 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -11,7 +11,7 @@ import EasyTipView import LucidBanner import MoEngageInApps -class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionHeaderMenuDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate, NCEmptyDataSetDelegate { +class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionFirstHeaderDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { @IBOutlet weak var collectionView: UICollectionView! @@ -21,6 +21,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let utilityFileSystem = NCUtilityFileSystem() let imageCache = NCImageCache.shared var dataSource = NCCollectionViewDataSource() + let networking = NCNetworking.shared let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() @@ -31,7 +32,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var backgroundImageView = UIImageView() var serverUrl: String = "" var isEditMode = false - var isDirectoryEncrypted = false + var isDirectoryE2EE = false var fileSelect: [String] = [] var metadataFolder: tableMetadata? var richWorkspaceText: String? @@ -40,7 +41,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var isSearchingMode: Bool = false var networkSearchInProgress: Bool = false var layoutForView: NCDBLayoutForView? - var dataSourceTask: URLSessionTask? + var searchDataSourceTask: URLSessionTask? var providers: [NKSearchProvider]? var searchResults: [NKSearchResult]? var listLayout = NCListLayout() @@ -56,123 +57,83 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // var headerMenu: NCSectionFirstHeader? var tipViewAccounts: EasyTipView? - var tipViewAutoUpload: EasyTipView? - var headerMenu: NCSectionHeaderMenu? - var headerMenuTransferView = false - var headerMenuButtonsView: Bool = true - var headerRichWorkspaceDisable: Bool = false - - // DECLARE - // - internal var layoutKey = "" - internal var titleCurrentFolder = "" - internal var titlePreviusFolder: String? - internal var enableSearchBar: Bool = false - internal var headerRichWorkspaceDisable: Bool = false - - internal var emptyImageName: String? - internal var emptyImageColors: [UIColor]? - internal var emptyTitle: String = "" + var selectableDataSource: [RealmSwiftObject] { dataSource.getMetadataSourceForAllSections() } - let maxImageGrid: CGFloat = 7 - var headerMenu: NCSectionHeaderMenu? - var headerMenuTransferView = false - var headerMenuButtonsView: Bool = true - var headerRichWorkspaceDisable: Bool = false - + // DECLARE + var layoutKey = "" + var titleCurrentFolder = "" + var titlePreviusFolder: String? + var enableSearchBar: Bool = false var groupByField = "name" + var headerRichWorkspaceDisable: Bool = false - var emptyImage: UIImage? + var emptyImageName: String? + var emptyImageColors: [UIColor]? + var emptyTitle: String = "" - internal var emptyDescription: String = "" - internal var emptyDataPortaitOffset: CGFloat = 0 - internal var emptyDataLandscapeOffset: CGFloat = -20 + var emptyDescription: String = "" + var emptyDataPortaitOffset: CGFloat = 0 + var emptyDataLandscapeOffset: CGFloat = -20 - internal var lastScale: CGFloat = 1.0 - internal var currentScale: CGFloat = 1.0 - internal var maxColumns: Int { + var lastScale: CGFloat = 1.0 + var currentScale: CGFloat = 1.0 + var maxColumns: Int { let screenWidth = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) let column = Int(screenWidth / 44) return column } - internal var transitionColumns = false - internal var numberOfColumns: Int = 0 - internal var lastNumberOfColumns: Int = 0 + var transitionColumns = false + var numberOfColumns: Int = 0 + var lastNumberOfColumns: Int = 0 - var isTransitioning: Bool = false - var selectableDataSource: [RealmSwiftObject] { dataSource.getMetadataSourceForAllSections() } - var pushed: Bool = false - var emptyDataSet: NCEmptyDataSet? - let heightHeaderRecommendations: CGFloat = 160 let heightHeaderSection: CGFloat = 30 + @MainActor var session: NCSession.Session { NCSession.shared.getSession(controller: tabBarController) } - internal var isLayoutPhoto: Bool { + var isLayoutPhoto: Bool { layoutForView?.layout == global.layoutPhotoRatio || layoutForView?.layout == global.layoutPhotoSquare } - internal var isLayoutGrid: Bool { + var isLayoutGrid: Bool { layoutForView?.layout == global.layoutGrid } - internal var isLayoutList: Bool { + var isLayoutList: Bool { layoutForView?.layout == global.layoutList } - internal var showDescription: Bool { + var showDescription: Bool { !headerRichWorkspaceDisable && NCPreferences().showDescription } - internal var isRecommendationActived: Bool { + var isRecommendationActived: Bool { let capabilities = NCNetworking.shared.capabilities[session.account] ?? NKCapabilities.Capabilities() return self.serverUrl == self.utilityFileSystem.getHomeServer(session: self.session) && capabilities.recommendations } - internal var infoLabelsSeparator: String { + var infoLabelsSeparator: String { layoutForView?.layout == global.layoutList ? " - " : "" } @MainActor - internal var session: NCSession.Session { - NCSession.shared.getSession(controller: tabBarController) - } - - @MainActor - internal var controller: NCMainTabBarController? { + var controller: NCMainTabBarController? { self.tabBarController as? NCMainTabBarController } - @MainActor - internal var mainNavigationController: NCMainNavigationController? { + var mainNavigationController: NCMainNavigationController? { self.navigationController as? NCMainNavigationController } - @MainActor - internal var sceneIdentifier: String { + var sceneIdentifier: String { (self.tabBarController as? NCMainTabBarController)?.sceneIdentifier ?? "" } - var defaultPredicate: NSPredicate { - let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, NCGlobal.shared.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) - return predicate - } - - var personalFilesOnlyPredicate: NSPredicate { - let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND (ownerId == %@ || ownerId == '') AND mountType == '' AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, session.userId, global.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) - return predicate - } - - @MainActor - internal var windowScene: UIWindowScene? { - SceneManager.shared.getWindowScene(controller: self.tabBarController as? NCMainTabBarController) - } - - internal var isNumberOfItemsInAllSectionsNull: Bool { + var isNumberOfItemsInAllSectionsNull: Bool { var totalItems = 0 for section in 0.. Bool { - let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" - let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") - return currentVersion != appVersion + + removeImageCache(metadatas: self.dataSource.getMetadatas()) } - func redirectToPrivacyViewController() { - let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) - let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController - newViewController.modalPresentationStyle = .fullScreen - self.present(newViewController, animated: true, completion: nil) + func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { + let viewController = presentationController.presentedViewController + + if viewController is NCViewerRichWorkspaceWebView { + closeRichWorkspaceWebView() + } } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) coordinator.animate(alongsideTransition: { _ in - self.collectionView?.collectionViewLayout.invalidateLayout() + let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) { + self.collectionView?.collectionViewLayout.invalidateLayout() + } + animator.startAnimation() }) self.dismissTip() @@ -518,20 +368,200 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS override var canBecomeFirstResponder: Bool { return true } + + @objc func updateIcons() { + collectionView.reloadData() + } - func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { - let viewController = presentationController.presentedViewController - - if viewController is NCViewerRichWorkspaceWebView { - closeRichWorkspaceWebView() + func isApplicationUpdated() -> Bool { + let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" + let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") + return currentVersion != appVersion + } + + func redirectToPrivacyViewController() { + let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) + let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as? UINavigationController + newViewController?.modalPresentationStyle = .fullScreen + self.present(newViewController!, animated: true, completion: nil) + } + + // MARK: - Transfer Delegate + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + + func transferChange(status: String, metadatasError: [tableMetadata: NKError]) { + switch status { + // DELETE + case self.global.networkingStatusDelete: + let errorForThisServer = metadatasError.first { entry in + let (key, value) = entry + return key.serverUrl == self.serverUrl && value != .success + }?.value + + let needLoadDataSource = metadatasError.contains { entry in + let (key, value) = entry + return key.serverUrl == self.serverUrl && value == .success + } + + if let error = errorForThisServer { + NCContentPresenter().showError(error: error) + } + + if self.isSearchingMode { + self.networkSearch() + } else if needLoadDataSource { + Task { + await self.reloadDataSource() + } + } else { + Task.detached { + if await self.isRecommendationActived() { + await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) + } + } + } + default: + break } } - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() + func transferChange(status: String, metadata: tableMetadata, error: NKError) { + guard session.account == metadata.account else { return } - if let frame = tabBarController?.tabBar.frame { - tabBarSelect?.hostingController?.view.frame = frame + if error != .success { + NCContentPresenter().showError(error: error) + } + + DispatchQueue.main.async { + switch status { + // UPLOADED, UPLOADED LIVEPHOTO + case self.global.networkingStatusUploaded, self.global.networkingStatusUploadedLivePhoto: + self.debouncer.call { + if self.isSearchingMode { + self.networkSearch() + } else if self.serverUrl == metadata.serverUrl { + Task { + await self.reloadDataSource() + } + } + } + // DOWNLOAD + case self.global.networkingStatusDownloading: + Task { + if metadata.serverUrl == self.serverUrl { + await self.reloadDataSource() + } + } + case self.global.networkingStatusDownloaded: + Task { + if metadata.serverUrl == self.serverUrl { + await self.reloadDataSource() + } + } + case self.global.networkingStatusDownloadCancel: + Task { + if metadata.serverUrl == self.serverUrl { + await self.reloadDataSource() + } + } + // CREATE FOLDER + case self.global.networkingStatusCreateFolder: + if metadata.serverUrl == self.serverUrl, metadata.sessionSelector != self.global.selectorUploadAutoUpload { + self.pushMetadata(metadata) + } + // RENAME + case self.global.networkingStatusRename: + self.debouncer.call { + if self.isSearchingMode { + self.networkSearch() + } else if self.serverUrl == metadata.serverUrl { + Task { + await self.reloadDataSource() + } + } + } + // FAVORITE + case self.global.networkingStatusFavorite: + self.debouncer.call { + if self.isSearchingMode { + self.networkSearch() + } else if self is NCFavorite { + Task { + await self.reloadDataSource() + } + } else if self.serverUrl == metadata.serverUrl { + Task { + await self.reloadDataSource() + } + } + } + default: + break + } + } + } + + func transferReloadData(serverUrl: String?, status: Int?) { + self.debouncer.call { + if self.isSearchingMode { + guard status != self.global.metadataStatusWaitDelete, + status != self.global.metadataStatusWaitRename, + status != self.global.metadataStatusWaitMove, + status != self.global.metadataStatusWaitCopy, + status != self.global.metadataStatusWaitFavorite else { + return + } + self.networkSearch() + } else if ( self.serverUrl == serverUrl) || serverUrl == nil { + Task { + await self.reloadDataSource() + } + } + } + } + + func transferRequestData(serverUrl: String?) { + self.debouncer.call { + if self.isSearchingMode { + self.networkSearch() + } else if ( self.serverUrl == serverUrl) || serverUrl == nil { + Task { + await self.getServerData() + } + } + } + } + + func transferCopy(metadata: tableMetadata, destination: String, error: NKError) { + if error != .success { + NCContentPresenter().showError(error: error) + } + + if isSearchingMode { + return networkSearch() + } + + if metadata.serverUrl == self.serverUrl || destination == self.serverUrl { + Task { + await self.reloadDataSource() + } + } + } + + func transferMove(metadata: tableMetadata, destination: String, error: NKError) { + if error != .success { + NCContentPresenter().showError(error: error) + } + + if isSearchingMode { + return networkSearch() + } + + if metadata.serverUrl == self.serverUrl || destination == self.serverUrl { + Task { + await self.reloadDataSource() + } } } @@ -640,837 +670,85 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // MARK: - NotificationCenter @objc func applicationWillResignActive(_ notification: NSNotification) { - self.mainNavigationController?.menuPlus?.resetPlusButtonAlpha() -// self.resetPlusButtonAlpha() -// self.refreshControl.endRefreshing() + mainNavigationController?.resetPlusButtonAlpha() } - @objc func reloadAvatar(_ notification: NSNotification) { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.showTipAccounts() + @objc func closeRichWorkspaceWebView() { + Task { + await self.reloadDataSource() } - guard let userInfo = notification.userInfo as NSDictionary?, - let error = userInfo["error"] as? NKError, - error.errorCode != global.errorNotModified else { return } - /// Magentacloud branding changes hide user account button on left navigation bar - setNavigationLeftItems() } - @objc func changeTheming(_ notification: NSNotification) { - self.reloadDataSource() - } + // MARK: - Layout + + func changeLayout(layoutForView: NCDBLayoutForView) { + let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) + let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 - @objc func changeLayout(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let account = userInfo["account"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let layoutForView = userInfo["layoutForView"] as? NCDBLayoutForView, - account == session.account, - serverUrl == self.serverUrl - else { return } + func changeLayout(withSubFolders: Bool) { + if self.layoutForView?.layout == layoutForView.layout { + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + Task { + await self.reloadDataSource() + } + return + } + + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + layoutForView.layout = layoutForView.layout + self.layoutType = layoutForView.layout + + collectionView.reloadData() + + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break + } + + self.collectionView.collectionViewLayout.invalidateLayout() - if serverUrl == homeServer || numFoldersLayoutsForView == 1 { Task { - await setLayout(layoutForView: layoutForView) - await self.reloadDataSource() + await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() } + } + + if serverUrl == homeServer || numFoldersLayoutsForView == 1 { + changeLayout(withSubFolders: false) } else { let alertController = UIAlertController(title: NSLocalizedString("_propagate_layout_", comment: ""), message: nil, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default, handler: { _ in - Task { - await self.setLayout(layoutForView: layoutForView, withSubFolders: true) - await self.reloadDataSource() - } + changeLayout(withSubFolders: true) })) alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in - Task { - await self.setLayout(layoutForView: layoutForView) - await self.reloadDataSource() - } + changeLayout(withSubFolders: false) })) -// if self.layoutForView?.layout == layoutForView.layout { -// self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) -// self.reloadDataSource() -// return -// } -// -// self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) -// layoutForView.layout = layoutForView.layout -// self.layoutType = layoutForView.layout -// -// collectionView.reloadData() - - switch layoutForView.layout { - case global.layoutList: - self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) - case global.layoutGrid: - self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) - case global.layoutPhotoSquare, global.layoutPhotoRatio: - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) - default: - break - } - self.collectionView.collectionViewLayout.invalidateLayout() - -// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() + self.present(alertController, animated: true) + } } - internal func setLayout(layoutForView: NCDBLayoutForView, withSubFolders: Bool = false) async { - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) - layoutForView.layout = layoutForView.layout - self.layoutType = layoutForView.layout - - collectionView.reloadData() - - switch layoutForView.layout { - case global.layoutList: - self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) - case global.layoutGrid: - self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) - case global.layoutPhotoSquare, global.layoutPhotoRatio: - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) - default: - break - } - - collectionView.collectionViewLayout.invalidateLayout() - } - - @objc func reloadDataSource(_ notification: NSNotification) { - if let userInfo = notification.userInfo as? NSDictionary { - if let serverUrl = userInfo["serverUrl"] as? String { - if serverUrl != self.serverUrl { - return - } - } - - if let clearDataSource = userInfo["clearDataSource"] as? Bool, clearDataSource { - self.dataSource.removeAll() - } - } - - reloadDataSource() - } - - @objc func getServerData(_ notification: NSNotification) { - if let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String { - if serverUrl != self.serverUrl { - return - } - } - - getServerData() - } - - @objc func reloadHeader(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let account = userInfo["account"] as? String, - account == session.account - else { return } - - self.collectionView.reloadData() - } - - @objc func changeStatusFolderE2EE(_ notification: NSNotification) { - reloadDataSource() - } - - @objc func closeRichWorkspaceWebView() { - reloadDataSource() - } - - @objc func deleteFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let error = userInfo["error"] as? NKError else { return } - - if error == .success { - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - } else { - NCContentPresenter().showError(error: error) - } - - reloadDataSource() - } - - @objc func copyMoveFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - account == session.account else { return } - - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - - if serverUrl == self.serverUrl { - reloadDataSource() - } - - @objc func reloadAvatar(_ notification: NSNotification) { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.showTipAccounts() - } - guard let userInfo = notification.userInfo as NSDictionary?, - let error = userInfo["error"] as? NKError, - error.errorCode != global.errorNotModified else { return } - /// Magentacloud branding changes hide user account button on left navigation bar - setNavigationLeftItems() - } - - @objc func renameFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let account = userInfo["account"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let error = userInfo["error"] as? NKError, - account == session.account - else { return } - - if error == .success { - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - } - - if serverUrl == self.serverUrl { - if error != .success { - NCContentPresenter().showError(error: error) - } - reloadDataSource() - } else { - collectionView.reloadData() - } - } - - @objc func createFolder(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let account = userInfo["account"] as? String, - account == session.account, - let withPush = userInfo["withPush"] as? Bool, - let metadata = database.getMetadataFromOcId(ocId) - else { return } - - if isSearchingMode { - return networkSearch() - } - - if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { - reloadDataSource() - } else if withPush, metadata.serverUrl == self.serverUrl { - reloadDataSource() - if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { - if sceneIdentifier == controller?.sceneIdentifier { - pushMetadata(metadata) - } - } else { - pushMetadata(metadata) - } - } - } - - @objc func favoriteFile(_ notification: NSNotification) { - if isSearchingMode { - return networkSearch() - } - - if self is NCFavorite { - return reloadDataSource() - } - - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - serverUrl == self.serverUrl - else { return } - - reloadDataSource() - } - - @objc func downloadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func downloadedFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func downloadCancelFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - !isSearchingMode, - let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) - else { return } - - // Header view trasfer - if metadata.isTransferInForeground { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) - DispatchQueue.main.async { self.collectionView?.reloadData() } - } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadedFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadedLivePhoto(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadCancelFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func triggerProgressTask(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let progressNumber = userInfo["progress"] as? NSNumber, - let totalBytes = userInfo["totalBytes"] as? Int64, - let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, - let ocId = userInfo["ocId"] as? String, - let ocIdTransfer = userInfo["ocIdTransfer"] as? String, - let session = userInfo["session"] as? String - else { return } - - let chunk: Int = userInfo["chunk"] as? Int ?? 0 - let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false - - let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) - -// DispatchQueue.main.async { - if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { - if NCNetworking.shared.transferInForegorund?.ocId == ocId { - NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue - } else { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) - self.collectionView.reloadData() - } - self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue - } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId), - let cell = self.collectionView?.cellForItem(at: indexPath), - let cell = cell as? NCCellProtocol else { return } - if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { - cell.fileProgressView?.isHidden = true - cell.fileProgressView?.progress = .zero - cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) - if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { - cell.writeInfoDateSize(date: metadata.date, size: metadata.size) - } else { - cell.fileInfoLabel?.text = "" - cell.fileSubinfoLabel?.text = "" - } - } else { - cell.fileProgressView?.isHidden = false - cell.fileProgressView?.progress = progressNumber.floatValue - cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) - let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal - if status == NCGlobal.shared.metadataStatusDownloading { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) - } else if status == NCGlobal.shared.metadataStatusUploading { - if totalBytes > 0 { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) - } else { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" - } - } - } - } -// } - } - - @objc func updateShare(_ notification: NSNotification) { - if isSearchingMode { - networkSearch() - } else { -// self.dataSource.removeAll() - getServerData() - } - } - - @objc func updateIcons() { - collectionView.reloadData() -// reloadDataSource() - } - - // MARK: - Layout - - func changeLayout(layoutForView: NCDBLayoutForView) { - let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) - let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 - - - if self.layoutForView?.layout == layoutForView.layout { - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) - self.reloadDataSource() - return - } - - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) - layoutForView.layout = layoutForView.layout - self.layoutType = layoutForView.layout - - collectionView.reloadData() - - switch layoutForView.layout { - case global.layoutList: - self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) - case global.layoutGrid: - self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) - case global.layoutPhotoSquare, global.layoutPhotoRatio: - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) - default: - break - } - - self.collectionView.collectionViewLayout.invalidateLayout() - -// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() - } - - @objc func reloadDataSource(_ notification: NSNotification) { - if let userInfo = notification.userInfo as? NSDictionary { - if let serverUrl = userInfo["serverUrl"] as? String { - if serverUrl != self.serverUrl { - return - } - } - - if let clearDataSource = userInfo["clearDataSource"] as? Bool, clearDataSource { - self.dataSource.removeAll() - } - } - - reloadDataSource() - } - - @objc func getServerData(_ notification: NSNotification) { - if let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String { - if serverUrl != self.serverUrl { - return - } - } - - getServerData() - } - - @objc func reloadHeader(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let account = userInfo["account"] as? String, - account == session.account - else { return } - - self.collectionView.reloadData() - } - - @objc func changeStatusFolderE2EE(_ notification: NSNotification) { - reloadDataSource() - } - - @objc func closeRichWorkspaceWebView() { - reloadDataSource() - } - - @objc func deleteFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let error = userInfo["error"] as? NKError else { return } - - if error == .success { - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - } else { - NCContentPresenter().showError(error: error) - } - - reloadDataSource() - } - - @objc func copyMoveFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - account == session.account else { return } - - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - - if serverUrl == self.serverUrl { - reloadDataSource() - } - } - - @objc func renameFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let account = userInfo["account"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let error = userInfo["error"] as? NKError, - account == session.account - else { return } - - if error == .success { - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - } - - if serverUrl == self.serverUrl { - if error != .success { - NCContentPresenter().showError(error: error) - } - reloadDataSource() - } else { - collectionView.reloadData() - } - } - - @objc func createFolder(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let account = userInfo["account"] as? String, - account == session.account, - let withPush = userInfo["withPush"] as? Bool, - let metadata = database.getMetadataFromOcId(ocId) - else { return } - - if isSearchingMode { - return networkSearch() - } - - if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { - reloadDataSource() - } else if withPush, metadata.serverUrl == self.serverUrl { - reloadDataSource() - if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { - if sceneIdentifier == controller?.sceneIdentifier { - pushMetadata(metadata) - } - } else { - pushMetadata(metadata) - } - } - } - - @objc func favoriteFile(_ notification: NSNotification) { - if isSearchingMode { - return networkSearch() - } - - if self is NCFavorite { - return reloadDataSource() - } - - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - serverUrl == self.serverUrl - else { return } - - reloadDataSource() - } - - @objc func downloadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func downloadedFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func downloadCancelFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - !isSearchingMode, - let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) - else { return } - - // Header view trasfer - if metadata.isTransferInForeground { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) - DispatchQueue.main.async { self.collectionView?.reloadData() } - } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadedFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadedLivePhoto(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadCancelFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func triggerProgressTask(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let progressNumber = userInfo["progress"] as? NSNumber, - let totalBytes = userInfo["totalBytes"] as? Int64, - let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, - let ocId = userInfo["ocId"] as? String, - let ocIdTransfer = userInfo["ocIdTransfer"] as? String, - let session = userInfo["session"] as? String - else { return } - - let chunk: Int = userInfo["chunk"] as? Int ?? 0 - let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false - - let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) - -// DispatchQueue.main.async { - if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { - if NCNetworking.shared.transferInForegorund?.ocId == ocId { - NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue - } else { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) - self.collectionView.reloadData() - } - self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue - } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId), - let cell = self.collectionView?.cellForItem(at: indexPath), - let cell = cell as? NCCellProtocol else { return } - if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { - cell.fileProgressView?.isHidden = true - cell.fileProgressView?.progress = .zero - cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) - if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { - cell.writeInfoDateSize(date: metadata.date, size: metadata.size) - } else { - cell.fileInfoLabel?.text = "" - cell.fileSubinfoLabel?.text = "" - } - } else { - cell.fileProgressView?.isHidden = false - cell.fileProgressView?.progress = progressNumber.floatValue - cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) - let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal - if status == NCGlobal.shared.metadataStatusDownloading { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) - } else if status == NCGlobal.shared.metadataStatusUploading { - if totalBytes > 0 { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) - } else { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" - } - } - } - } -// } - } - - @objc func updateShare(_ notification: NSNotification) { - if isSearchingMode { - networkSearch() - } else { -// self.dataSource.removeAll() - getServerData() - } - } - - @objc func updateIcons() { - collectionView.reloadData() -// reloadDataSource() - } - - // MARK: - Layout - - func setNavigationLeftItems() { - navigationItem.title = titleCurrentFolder - } - func getNavigationTitle() -> String { - let tableAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) - if let tableAccount, - !tableAccount.alias.isEmpty { - return tableAccount.alias + let tblAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) + if let tblAccount, + !tblAccount.alias.isEmpty { + return tblAccount.alias } return NCBrandOptions.shared.brand } - func accountSettingsDidDismiss(tableAccount: tableAccount?, controller: NCMainTabBarController?) { } + func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } @MainActor func startGUIGetServerData() { self.dataSource.setGetServerData(false) + self.collectionView.reloadData() // Don't show spinner on iPad root folder if UIDevice.current.userInterfaceIdiom == .pad, @@ -1533,83 +811,90 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS view.emptyDescription.text = NSLocalizedString("_no_file_pull_down_", comment: "") } } + + let spinner = UIActivityIndicatorView(style: .medium) + spinner.startAnimating() + + let container = UIView() + container.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(spinner) + + spinner.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + spinner.centerXAnchor.constraint(equalTo: container.centerXAnchor), + spinner.centerYAnchor.constraint(equalTo: container.centerYAnchor) + ]) + + self.navigationItem.titleView = container } - + + @MainActor + func restoreDefaultTitle() { + self.navigationItem.titleView = nil + self.navigationItem.title = self.titleCurrentFolder + } + // MARK: - SEARCH func searchController(enabled: Bool) { - guard enableSearchBar else { - return - } + guard enableSearchBar else { return } searchController?.searchBar.isUserInteractionEnabled = enabled - if enabled { searchController?.searchBar.alpha = 1 } else { searchController?.searchBar.alpha = 0.3 + } } func updateSearchResults(for searchController: UISearchController) { - searchResultText = searchController.searchBar.text + self.literalSearch = searchController.searchBar.text } func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + isSearchingMode = true + self.providers?.removeAll() + self.dataSource.removeAll() + Task { + await self.reloadDataSource() + } // TIP dismissTip() - - // (+) - self.mainNavigationController?.menuPlus?.hiddenPlusButton(true) - - if !isSearchingMode { - self.isSearchingMode = true - self.dataSource.removeAll() - self.collectionView.reloadData() - } -// isSearchingMode = true -// self.providers?.removeAll() -// self.dataSource.removeAll() -// self.reloadDataSource() -// // TIP -// dismissTip() + // + mainNavigationController?.hiddenPlusButton(true) } func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { - if isSearchingMode, - searchResultText?.count ?? 0 >= 2 { - Task { - await self.search() - } -// if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { -// networkSearch() + if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { + networkSearch() } } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - NCNetworking.shared.cancelUnifiedSearchFiles() + self.networking.cancelUnifiedSearchFiles() + self.isSearchingMode = false self.literalSearch = "" self.providers?.removeAll() self.dataSource.removeAll() - self.reloadDataSource() + Task { + await self.reloadDataSource() + } + // + mainNavigationController?.hiddenPlusButton(false) } // MARK: - TAP EVENT - func tapMoreListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { - tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) - } - func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) } - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { - tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) } func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) { - if isEditMode { return } guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) @@ -1618,29 +903,11 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS appDelegate.adjust.trackEvent(TriggerEvent(Sharing.rawValue)) } - func tapMoreGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { - if isEditMode { return } - guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } -// toggleMenu(metadata: metadata, image: image) - if namedButtonMore == NCGlobal.shared.buttonMoreMore || namedButtonMore == NCGlobal.shared.buttonMoreLock { - toggleMenu(metadata: metadata, image: image) - } else if namedButtonMore == NCGlobal.shared.buttonMoreStop { - Task { - await cancelSession(metadata: metadata) - } - } - } - func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } -// toggleMenu(metadata: metadata, image: image) - Task { - await cancelSession(metadata: metadata) - } + toggleMenu(metadata: metadata, image: image, sender: sender) } - func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func tapRichWorkspace(_ sender: Any) { if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { @@ -1654,97 +921,28 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } - func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - // (+) - self.mainNavigationController?.menuPlus?.hiddenPlusButton(false) - - self.isSearchingMode = false - self.networkSearchInProgress = false - self.searchResultText = nil - self.searchResultStore = nil - - Task { - await searchOperationHandle.cancel() - await reloadDataSource() - - // Restore Layout - if let layoutForViewLayoutStore { - let layoutForView = database.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) - layoutForView.layout = layoutForViewLayoutStore - await setLayout(layoutForView: layoutForView) - } - layoutForViewLayoutStore = nil - - // update Option menu - await mainNavigationController?.updateMenuOption() - } - } - - func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?) { - toggleMenu(metadata: metadata, image: image) + func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { + toggleMenu(metadata: metadata, image: image, sender: sender) } - @MainActor - func setSearchBarLoading(_ loading: Bool) { - guard let textField = searchController?.searchBar.searchTextField else { - return - } - if loading { - let spinner = UIActivityIndicatorView(style: .medium) - spinner.startAnimating() - textField.rightView = spinner - textField.rightViewMode = .always - } else { - textField.rightView = nil - } + func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { + unifiedSearchMore(metadataForSection: metadataForSection) } - // MARK: - TAP EVENT - func tapRecommendations(with metadata: tableMetadata) { didSelectMetadata(metadata, withOcIds: false) } - func tapButtonSwitch(_ sender: Any) { - guard !isTransitioning else { return } - isTransitioning = true - - guard let layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) else { return } - - if layoutForView.layout == NCGlobal.shared.layoutGrid { - layoutForView.layout = NCGlobal.shared.layoutList - } else { - layoutForView.layout = NCGlobal.shared.layoutGrid - } - self.layoutForView = NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) - self.collectionView.reloadData() - self.collectionView.collectionViewLayout.invalidateLayout() - self.collectionView.setCollectionViewLayout(layoutForView.layout == NCGlobal.shared.layoutList ? self.listLayout : self.gridLayout, animated: true) {_ in self.isTransitioning = false } - } - - func tapButtonOrder(_ sender: Any) { - let sortMenu = NCSortMenu() - sortMenu.toggleMenu(viewController: self, account: session.account, key: layoutKey, sortButton: sender as? UIButton, serverUrl: serverUrl) - } - - func tapButtonTransfer(_ sender: Any) { } - - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } - func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func longPressListItem(with objectId: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) { - } - func longPressListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func longPressGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func longPressMoreListItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - - func longPressGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } - - func longPressMoreGridItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } - + + func longPressMoreGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) } @@ -1766,7 +964,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS becomeFirstResponder() if !serverUrl.isEmpty { - listMenuItems.append(UIMenuItem(title: NSLocalizedString("_paste_file_", comment: ""), action: #selector(pasteFilesMenu))) + listMenuItems.append(UIMenuItem(title: NSLocalizedString("_paste_file_", comment: ""), action: #selector(pasteFilesMenu(_:)))) } if !listMenuItems.isEmpty { @@ -1775,23 +973,16 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } - // MARK: - Transfer Delegate - - func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } - - func tranferChange(status: String, metadata: tableMetadata, error: NKError) { } - // MARK: - Menu Item override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - - if #selector(pasteFilesMenu) == action { + if #selector(pasteFilesMenu(_:)) == action { if !UIPasteboard.general.items.isEmpty, !(metadataFolder?.e2eEncrypted ?? false) { return true } - } else if #selector(copyMenuFile) == action { + } else if #selector(copyMenuFile(_:)) == action { return true - } else if #selector(moveMenuFile) == action { + } else if #selector(moveMenuFile(_:)) == action { return true } @@ -1894,95 +1085,168 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) } - await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: self.serverUrl) + DispatchQueue.main.async { +// UIView.transition(with: self.collectionView, +// duration: 0.20, +// options: .transitionCrossDissolve, +// animations: { self.collectionView.reloadData() }, +// completion: nil) +// +// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() + if isRecommendationActived { + Task.detached { + await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) + } + } } - await mainNavigationController?.updateMenuOption() - (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + DispatchQueue.main.async { +// UIView.transition(with: self.collectionView, +// duration: 0.20, +// options: .transitionCrossDissolve, +// animations: { self.collectionView.reloadData() }, +// completion: nil) +// +// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() self.refreshControl.endRefreshing() self.collectionView.reloadData() self.setNavigationRightItems() } } - func getServerData() { - } - - // MARK: - Push metadata + func getServerData(forced: Bool = false) async { } - /// Pushes or reuses the folder view controller associated with the provided metadata. - /// - /// - Parameter metadata: The metadata representing the selected folder. - @MainActor - func pushMetadata(_ metadata: tableMetadata) async { - guard !isNavigatingMetadata, - let navigationController = self.navigationController, - let navigationCollectionViewCommon = self.controller?.navigationCollectionViewCommon else { + @objc func networkSearch() { + guard !networkSearchInProgress else { return } - - isNavigatingMetadata = true - defer { - isNavigatingMetadata = false + guard !session.account.isEmpty, + let literalSearch = literalSearch, + !literalSearch.isEmpty else { + return } + let capabilities = NCNetworking.shared.capabilities[session.account] ?? NKCapabilities.Capabilities() - let serverUrlPush = utilityFileSystem.createServerUrl( - serverUrl: metadata.serverUrl, - fileName: metadata.fileName - ) - - // Update the last opening date without blocking the main flow. - Task.detached(priority: .utility) { [database] in - await database.setDirectoryLastOpeningDateAsync(ocId: metadata.ocId) + self.networkSearchInProgress = true + self.dataSource.removeAll() + Task { + await self.reloadDataSource() } - guard navigationController.transitionCoordinator == nil else { - return + if capabilities.serverVersionMajor >= global.nextcloudVersion20 { + self.networking.unifiedSearchFiles(literal: literalSearch, account: session.account) { task in + self.searchDataSourceTask = task + Task { + await self.reloadDataSource() + } + } providers: { account, searchProviders in + self.providers = searchProviders + self.searchResults = [] + self.dataSource = NCCollectionViewDataSource(metadatas: [], + layoutForView: self.layoutForView, + providers: self.providers, + searchResults: self.searchResults, + account: account) + } update: { _, _, searchResult, metadatas in + guard let metadatas, !metadatas.isEmpty, self.isSearchingMode, let searchResult else { return } + self.networking.unifiedSearchQueue.addOperation(NCCollectionViewUnifiedSearch(collectionViewCommon: self, metadatas: metadatas, searchResult: searchResult)) + } completion: { _, _ in + Task { + await self.reloadDataSource() + } + self.networkSearchInProgress = false + } + } else { + self.networking.searchFiles(literal: literalSearch, account: session.account) { task in + self.searchDataSourceTask = task + Task { + await self.reloadDataSource() + } + } completion: { metadatasSearch, error in + Task { + guard let metadatasSearch, + error == .success, + self.isSearchingMode + else { + self.networkSearchInProgress = false + await self.reloadDataSource() + return + } + let ocId = metadatasSearch.map { $0.ocId } + let metadatas = await self.database.getMetadatasAsync(predicate: NSPredicate(format: "ocId IN %@", ocId), + withLayout: self.layoutForView, + withAccount: self.session.account) + + self.dataSource = NCCollectionViewDataSource(metadatas: metadatas, + layoutForView: self.layoutForView, + providers: self.providers, + searchResults: self.searchResults, + account: self.session.account) + self.networkSearchInProgress = false + await self.reloadDataSource() + } + } } + } - if let existingEntry = navigationCollectionViewCommon.first(where: { - $0.navigationController === navigationController && $0.serverUrl == serverUrlPush - }) { - let viewController = existingEntry.viewController + func unifiedSearchMore(metadataForSection: NCMetadataForSection?) { + guard let metadataForSection = metadataForSection, let lastSearchResult = metadataForSection.lastSearchResult, let cursor = lastSearchResult.cursor, let term = literalSearch else { return } - if navigationController.topViewController === viewController { - return - } + metadataForSection.unifiedSearchInProgress = true + self.collectionView?.reloadData() - if navigationController.viewControllers.contains(where: { $0 === viewController }) { - navigationController.popToViewController(viewController, animated: true) - return + self.networking.unifiedSearchFilesProvider(id: lastSearchResult.id, term: term, limit: 5, cursor: cursor, account: session.account) { task in + self.searchDataSourceTask = task + Task { + await self.reloadDataSource() + } + } completion: { _, searchResult, metadatas, error in + if error != .success { + Task {@MainActor in + await showErrorBanner( + controller: self.controller, + errorDescription: error.errorDescription, + errorCode: error.errorCode + ) + } } - navigationController.pushViewController(viewController, animated: true) - return + metadataForSection.unifiedSearchInProgress = false + guard let searchResult = searchResult, let metadatas = metadatas else { return } + self.dataSource.appendMetadatasToSection(metadatas, metadataForSection: metadataForSection, lastSearchResult: searchResult) + + DispatchQueue.main.async { + self.collectionView?.reloadData() + } } + } + + // MARK: - Push metadata - guard let viewController = UIStoryboard(name: "NCFiles", bundle: nil).instantiateInitialViewController() as? NCFiles else { + func pushMetadata(_ metadata: tableMetadata) { + guard let navigationCollectionViewCommon = self.controller?.navigationCollectionViewCommon else { return } + let serverUrlPush = utilityFileSystem.createServerUrl(serverUrl: metadata.serverUrl, fileName: metadata.fileName) - viewController.serverUrl = serverUrlPush - viewController.titlePreviusFolder = navigationItem.title - viewController.titleCurrentFolder = metadata.fileNameView + // Set Last Opening Date + Task { + await database.setDirectoryLastOpeningDateAsync(ocId: metadata.ocId) + } - navigationCollectionViewCommon.append( - NavigationCollectionViewCommon( - serverUrl: serverUrlPush, - navigationController: navigationController, - viewController: viewController - ) - ) + if let viewController = navigationCollectionViewCommon.first(where: { $0.navigationController == self.navigationController && $0.serverUrl == serverUrlPush})?.viewController, viewController.isViewLoaded { + navigationController?.pushViewController(viewController, animated: true) + } else { + if let viewController: NCFiles = UIStoryboard(name: "NCFiles", bundle: nil).instantiateInitialViewController() as? NCFiles { + viewController.serverUrl = serverUrlPush + viewController.titlePreviusFolder = navigationItem.title + viewController.titleCurrentFolder = metadata.fileNameView - navigationController.pushViewController(viewController, animated: true) - } - - func pushViewController(viewController: UIViewController) { - if pushed { return } + navigationCollectionViewCommon.append(NavigationCollectionViewCommon(serverUrl: serverUrlPush, navigationController: self.navigationController, viewController: viewController)) - pushed = true - navigationController?.pushViewController(viewController, animated: true) + navigationController?.pushViewController(viewController, animated: true) + } + } } func pushViewController(viewController: UIViewController) { @@ -2001,7 +1265,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS !results.isEmpty { return Array(results) } - return nil } func sizeForHeaderInSection(section: Int) -> CGSize { @@ -2010,12 +1273,12 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let isIphone = UIDevice.current.userInterfaceIdiom == .phone if self.dataSource.isEmpty() { - height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset, isHeaderMenuTransferViewEnabled: isHeaderMenuTransferViewEnabled() != nil) + height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset) } else if isEditMode || (isLandscape && isIphone) { return CGSize.zero } else { - let (heightHeaderCommands, heightHeaderRichWorkspace, heightHeaderSection) = getHeaderHeight(section: section) - height = heightHeaderCommands + heightHeaderRichWorkspace + heightHeaderSection + let (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) = getHeaderHeight(section: section) + height = heightHeaderRichWorkspace + heightHeaderRecommendations + heightHeaderSection } return CGSize(width: collectionView.frame.width, height: height) @@ -2024,137 +1287,25 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // MARK: - Footer size func sizeForFooterInSection(section: Int) -> CGSize { - let sections = dataSource.numberOfSections() - let metadataForSection = self.dataSource.getMetadataForSection(section) - let isPaginated = metadataForSection?.lastSearchResult?.isPaginated ?? false - let metadatasCount: Int = metadataForSection?.lastSearchResult?.entries.count ?? 0 - var size = CGSize(width: collectionView.frame.width, height: 0) - - if section == sections - 1 { - size.height += 85 - } else { - size.height += 1 - } - - if isSearchingMode && isPaginated && metadatasCount > 0 { - size.height += 30 - } - return size - } - - func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } -} - -extension NCCollectionViewCommon: NCSectionFirstHeaderDelegate { - func tapRichWorkspace(_ sender: Any) { - if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { - if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { - viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" - viewerRichWorkspace.serverUrl = serverUrl - viewerRichWorkspace.delegate = self - - navigationController.modalPresentationStyle = .fullScreen - self.present(navigationController, animated: true, completion: nil) - } - } - } - - func tapRecommendations(with metadata: tableMetadata) { - Task { - await didSelectMetadata(metadata, withOcIds: false) - } - } -} - -extension NCCollectionViewCommon: NCSectionFooterDelegate { - func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { - Task { - await unifiedSearchMore(metadataForSection: metadataForSection) + guard let controller else { + return CGSize.zero } - } -} - -// MARK: - Transfer Delegate - -extension NCCollectionViewCommon: NCTransferDelegate { - func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + let sections = dataSource.numberOfSections() + let bottomAreaInsets: CGFloat = controller.tabBar.safeAreaInsets.bottom == 0 ? 34 : 0 + let height = controller.tabBar.frame.height + bottomAreaInsets - func transferReloadData(serverUrl: String?) { - Task { - await self.debouncerReloadData.call({ - self.collectionView.reloadData() - }, immediate: true) + if isEditMode { + return CGSize(width: collectionView.frame.width, height: 90 + height) } - } - - func transferChange(status: String, - account: String, - fileName: String, - serverUrl: String, - selector: String?, - ocId: String, - destination: String?, - error: NKError) { - Task { - if error != .success, - error.errorCode != global.errorResourceNotFound { - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) - } - - guard session.account == account else { - return - } - - if self.isSearchingMode { - await self.debouncerNetworkSearch.call { - await self.search() - } - return - } - switch status { - case self.global.networkingStatusCreateFolder: - if error == .success, - serverUrl == self.serverUrl, - selector != self.global.selectorUploadAutoUpload, - let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { - if metadata.e2eEncrypted { - await self.reloadDataSource() - } else { - await self.pushMetadata(metadata) - } - } - default: - if self.serverUrl == serverUrl || destination == self.serverUrl || self.serverUrl.isEmpty { - await self.debouncerReloadDataSource.call { - await self.reloadDataSource() - } - } - } + if isSearchingMode { + return CGSize(width: collectionView.frame.width, height: 50) } - } - func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { - Task { - if self.isSearchingMode { - await self.debouncerNetworkSearch.call { - await self.search() - } - return - } - - if requestData && (self.serverUrl == serverUrl || serverUrl == nil) { - await self.debouncerGetServerData.call { - await self.getServerData() - } - return - } - - if self.serverUrl == serverUrl || serverUrl == nil { - await self.debouncerReloadDataSource.call { - await self.reloadDataSource() - } - } + if section == sections - 1 { + return CGSize(width: collectionView.frame.width, height: height) + } else { + return CGSize(width: collectionView.frame.width, height: 0) } } } diff --git a/iOSClient/NCBackgroundLocationUploadManager.swift b/iOSClient/NCBackgroundLocationUploadManager.swift index 154fda662d..8f63b82e9d 100644 --- a/iOSClient/NCBackgroundLocationUploadManager.swift +++ b/iOSClient/NCBackgroundLocationUploadManager.swift @@ -1,6 +1,10 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2025 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCBackgroundLocationUploadManager.swift +// Nextcloud +// +// Created by Marino Faggiana on 06/06/25. +// Copyright © 2025 Marino Faggiana. All rights reserved. +// import CoreLocation import NextcloudKit @@ -102,16 +106,15 @@ class NCBackgroundLocationUploadManager: NSObject, CLLocationManagerDelegate { } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - // Must work only in background guard isAppInBackground else { return } // Open Realm - guard NCManageDatabase.shared.openRealmBackground() else { - nkLog(tag: self.global.logTagLocation, emoji: .error, message: "Failed to open Realm in Location Manager") - return - } + if database.openRealmBackground() { + let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! + let location = locations.last + nkLog(tag: self.global.logTagLocation, emoji: .start, message: "Triggered by location change: \(location?.coordinate.latitude ?? 0), \(location?.coordinate.longitude ?? 0)") let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! let location = locations.last diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index 4065f60652..c7c11d0a81 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -50,7 +50,7 @@ final class NCGlobal: Sendable { // let avatarSize: Int = 128 * Int(UIScreen.main.scale) let avatarSizeRounded: Int = 128 - + // Preview size // let size1024: CGSize = CGSize(width: 1024, height: 1024) @@ -104,7 +104,6 @@ final class NCGlobal: Sendable { let layoutViewOffline = "LayoutOffline" let layoutViewFavorite = "LayoutFavorite" let layoutViewFiles = "LayoutFiles" - let layoutViewTransfers = "LayoutTransfers" let layoutViewRecent = "LayoutRecent" let layoutViewShares = "LayoutShares" let layoutViewShareExtension = "LayoutShareExtension" @@ -126,6 +125,7 @@ final class NCGlobal: Sendable { let heightFooterButton: CGFloat = 30 let endHeightFooter: CGFloat = 85 + // Text - OnlyOffice - Collabora - QuickLook // let editorText = "text" @@ -229,7 +229,7 @@ final class NCGlobal: Sendable { let selectorSynchronizationOffline = "synchronizationOffline" let selectorPrint = "print" let selectorDeleteFile = "deleteFile" - + // Metadata : Status // // 0 normal @@ -259,11 +259,9 @@ final class NCGlobal: Sendable { let metadataStatusForScreenAwake = [-1, -2, 1, 2] let metadataStatusHideInView = [1, 2, 3, 11] let metadataStatusWaitWebDav = [10, 11, 12, 13, 14, 15] - let metadataStatusTransfers = [-2, -3, 2, 3, 10, 11, 12, 13, 14, 15] let metadatasStatusInWaiting = [-1, 1, 10, 11, 12, 13, 14, 15] - let metadatasStatusInWaitingDownloadUpload = [-1, 1] - let metadatasStatusDownloadingUploading = [-2, 2] + let metadatasStatusInProgress = [-2, 2] // Auto upload subfolder granularity // @@ -277,22 +275,16 @@ final class NCGlobal: Sendable { let notificationCenterChangeTheming = "changeTheming" // userInfo: account let notificationCenterRichdocumentGrabFocus = "richdocumentGrabFocus" let notificationCenterReloadDataNCShare = "reloadDataNCShare" + let notificationCenterDidCreateShareLink = "didCreateShareLink" + let notificationCenterCloseRichWorkspaceWebView = "closeRichWorkspaceWebView" let notificationCenterReloadAvatar = "reloadAvatar" let notificationCenterClearCache = "clearCache" let notificationCenterCheckUserDelaultErrorDone = "checkUserDelaultErrorDone" // userInfo: account, controller let notificationCenterServerDidUpdate = "serverDidUpdate" // userInfo: account let notificationCenterNetworkReachability = "networkReachability" - let notificationCenterDidCreateShareLink = "didCreateShareLink" - let notificationCenterDeleteFile = "deleteFile" // userInfo: [ocId], error - let notificationCenterCopyMoveFile = "copyMoveFile" // userInfo: [ocId] serverUrl, account, dragdrop, type (copy, move) - let notificationCenterMoveFile = "moveFile" // userInfo: [ocId], [indexPath], error - let notificationCenterCopyFile = "copyFile" // userInfo: [ocId], [indexPath], error let notificationCenterRenameFile = "renameFile" // userInfo: serverUrl, account, error - let notificationCenterFavoriteFile = "favoriteFile" // userInfo: ocId, serverUrl - let notificationCenterFileExists = "fileExists" // userInfo: ocId, fileExists - let notificationCenterReloadDataSource = "reloadDataSource" // userInfo: serverUrl?, clearDataSource let notificationCenterMenuSearchTextPDF = "menuSearchTextPDF" let notificationCenterMenuGotToPageInPDF = "menuGotToPageInPDF" @@ -300,7 +292,6 @@ final class NCGlobal: Sendable { let notificationCenterOpenMediaDetail = "openMediaDetail" // userInfo: ocId let notificationCenterDismissScanDocument = "dismissScanDocument" - let notificationCenterDismissUploadAssets = "dismissUploadAssets" let notificationCenterEnableSwipeGesture = "enableSwipeGesture" let notificationCenterDisableSwipeGesture = "disableSwipeGesture" @@ -425,7 +416,7 @@ final class NCGlobal: Sendable { let keyFileNameAutoUploadType = "fileNameAutoUploadType" let keyFileNameOriginal = "fileNameOriginal" let keyFileNameOriginalAutoUpload = "fileNameOriginalAutoUpload" - + // LOG TAG // let logTagTask = "BGT" @@ -444,10 +435,6 @@ final class NCGlobal: Sendable { // let udMigrationMultiDomains = "migrationMultiDomains" let udLastVersion = "lastVersion" - - // Album - // - let selectedTabIndexAlbum: Int = 3 } /** diff --git a/iOSClient/Networking/NCConfigServer.swift b/iOSClient/Networking/NCConfigServer.swift index f82b466647..d5e4d2a867 100644 --- a/iOSClient/Networking/NCConfigServer.swift +++ b/iOSClient/Networking/NCConfigServer.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2022 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCConfigServer.swift +// Nextcloud +// +// Created by Marino Faggiana on 05/12/22. +// Copyright © 2022 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import Foundation import UIKit diff --git a/iOSClient/Networking/NCNetworking+Download.swift b/iOSClient/Networking/NCNetworking+Download.swift index 4d9fe0c51c..22ecb91653 100644 --- a/iOSClient/Networking/NCNetworking+Download.swift +++ b/iOSClient/Networking/NCNetworking+Download.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCNetworking+Download.swift +// Nextcloud +// +// Created by Marino Faggiana on 07/02/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import UIKit import NextcloudKit @@ -327,3 +346,40 @@ extension NCNetworking { } } } + +class NCOperationDownload: ConcurrentOperation, @unchecked Sendable { + var metadata: tableMetadata + var selector: String + + init(metadata: tableMetadata, selector: String) { + self.metadata = tableMetadata.init(value: metadata) + self.selector = selector + } + + override func start() { + guard !isCancelled else { return self.finish() } + + metadata.session = NCNetworking.shared.sessionDownload + metadata.sessionError = "" + metadata.sessionSelector = selector + metadata.sessionTaskIdentifier = 0 + metadata.status = NCGlobal.shared.metadataStatusWaitDownload + +// let metadata = NCManageDatabase.shared.addMetadata(metadata) + +// NCNetworking.shared.download(metadata: metadata, withNotificationProgressTask: true) { +// } completion: { _, _ in +// self.finish() +// } + Task { + await download(withSelector: self.selector) + } + } + + private func download(withSelector selector: String = "") async { + await NCNetworking.shared.downloadFile(metadata: metadata) { _ in + self.finish() + } taskHandler: { _ in } + + } +} diff --git a/iOSClient/Networking/NCNetworking+LivePhoto.swift b/iOSClient/Networking/NCNetworking+LivePhoto.swift index e861b76b81..072b3b087f 100644 --- a/iOSClient/Networking/NCNetworking+LivePhoto.swift +++ b/iOSClient/Networking/NCNetworking+LivePhoto.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCNetworking+LivePhoto.swift +// Nextcloud +// +// Created by Marino Faggiana on 07/02/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import UIKit import NextcloudKit diff --git a/iOSClient/Networking/NCNetworking+Task.swift b/iOSClient/Networking/NCNetworking+Task.swift index f28830b583..1b6122252b 100644 --- a/iOSClient/Networking/NCNetworking+Task.swift +++ b/iOSClient/Networking/NCNetworking+Task.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCNetworking+Task.swift +// Nextcloud +// +// Created by Marino Faggiana on 24/08/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import Foundation import UIKit diff --git a/iOSClient/Networking/NCNetworking+Upload.swift b/iOSClient/Networking/NCNetworking+Upload.swift index 9467e9c0b7..6c985dbe7c 100644 --- a/iOSClient/Networking/NCNetworking+Upload.swift +++ b/iOSClient/Networking/NCNetworking+Upload.swift @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later - import UIKit import NextcloudKit import Alamofire @@ -45,6 +41,22 @@ extension NCNetworking { } taskHandler(task) } progressHandler: { progress in + Task { + guard await self.progressQuantizer.shouldEmit(serverUrlFileName: serverUrlFileName, fraction: progress.fractionCompleted) else { + return + } + + if let metadata { + await NCManageDatabase.shared.setMetadataProgress(ocId: metadata.ocId, progress: progress.fractionCompleted) + await self.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferProgressDidUpdate(progress: Float(progress.fractionCompleted), + totalBytes: progress.totalUnitCount, + totalBytesExpected: progress.completedUnitCount, + fileName: metadata.fileName, + serverUrl: metadata.serverUrl) + } + } + } progressHandler(progress.completedUnitCount, progress.totalUnitCount, progress.fractionCompleted) } @@ -378,6 +390,7 @@ extension NCNetworking { error: self.global.diagnosticProblemsUploadServerError) } } + await NCManageDatabase.shared.updateBadge() } // MARK: - diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 37629dadd1..4d78a1844e 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCNetworking+WebDAV.swift +// Nextcloud +// +// Created by Marino Faggiana on 07/02/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import UIKit import NextcloudKit @@ -465,6 +484,50 @@ extension NCNetworking { return .success } + + func renameMetadata(_ metadata: tableMetadata, + fileNameNew: String, + indexPath: IndexPath, + viewController: UIViewController?, + completion: @escaping (_ error: NKError) -> Void) { + + let permission = NCMetadataPermissions.permissionsContainsString(metadata.permissions, permissions: NCMetadataPermissions.permissionCanRename) + if (!metadata.permissions.isEmpty && permission == false) || + (metadata.status != global.metadataStatusNormal && metadata.status != global.metadataStatusWaitRename) { +// return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_no_permission_modify_file_")) +// let error = await NCNetworkingE2EERename().rename(metadata: metadata, fileNameNew: fileNameNew) +// DispatchQueue.main.async { completion(error) } + completion(NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_no_permission_modify_file_")) + } + + if metadata.isDirectoryE2EE { +#if !EXTENSION + if isOffline { +// return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) + completion(NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) + } + Task { + let error = await NCNetworkingE2EERename().rename(metadata: metadata, fileNameNew: fileNameNew) + if error != .success { +// NCContentPresenter().showError(error: error) + completion(error) + } + } +#endif + } else { + Task { + await self.transferDispatcher.notifyAllDelegatesAsync { delegate in + let status = self.global.metadataStatusWaitRename + await NCManageDatabase.shared.renameMetadata(fileNameNew: fileNameNew, ocId: metadata.ocId, status: status) + delegate.transferReloadData(serverUrl: metadata.serverUrl, status: status) + } + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterRenameFile, userInfo: ["serverUrl": metadata.serverUrl, "account": metadata.account, "error": NKError(errorCode: 0, errorDescription: ""), "ocId": metadata.ocId, "indexPath": indexPath]) + + } + + completion(NKError(errorCode: 0, errorDescription: "")) + } + } func renameFileOrFolder(metadata: tableMetadata) async -> NKError { let serverUrlFileNameSource = metadata.serverUrlFileName diff --git a/iOSClient/Networking/NCNetworking.swift b/iOSClient/Networking/NCNetworking.swift index c0684756d4..0c86cf84c8 100644 --- a/iOSClient/Networking/NCNetworking.swift +++ b/iOSClient/Networking/NCNetworking.swift @@ -37,6 +37,203 @@ protocol NCTransferDelegate: AnyObject { serverUrl: String) } +extension NCTransferDelegate { + func transferChange(status: String, + account: String, + fileName: String, + serverUrl: String, + selector: String?, + ocId: String, + destination: String?, + error: NKError) {} + func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) {} + func transferProgressDidUpdate(progress: Float, + totalBytes: Int64, + totalBytesExpected: Int64, + fileName: String, + serverUrl: String) {} +} + +/// Actor-based delegate dispatcher using weak references. +actor NCTransferDelegateDispatcher { + // Weak reference collection of delegates + private var transferDelegates = NSHashTable.weakObjects() + + /// Adds a delegate safely. + func addDelegate(_ delegate: NCTransferDelegate) { + transferDelegates.add(delegate) + } + + /// Remove a delegate safely. + func removeDelegate(_ delegate: NCTransferDelegate) { + transferDelegates.remove(delegate) + } + + /// Notifies all delegates. + func notifyAllDelegates(_ block: (NCTransferDelegate) -> Void) { + let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + for delegate in delegatesCopy { + block(delegate) + } + } + + func notifyAllDelegatesAsync(_ block: @escaping (NCTransferDelegate) async -> Void) async { + let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + for delegate in delegatesCopy { + await block(delegate) + } + } + + /// Notifies the delegate for a specific scene. + func notifyDelegate(forScene sceneIdentifier: String, _ block: (NCTransferDelegate) -> Void) { + let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + for delegate in delegatesCopy { + if delegate.sceneIdentifier == sceneIdentifier { + block(delegate) + } + } + } + + /// Notifies matching and non-matching delegates for a specific scene. + func notifyDelegates(forScene sceneIdentifier: String, + matching: (NCTransferDelegate) -> Void, + others: (NCTransferDelegate) -> Void) { + let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + for delegate in delegatesCopy { + if delegate.sceneIdentifier == sceneIdentifier { + matching(delegate) + } else { + others(delegate) + } + } + } +} + +/// A thread-safe registry for tracking in-flight `URLSessionTask` instances. +/// +/// Each task is associated with a string identifier (`identifier`) that you define, +/// allowing you to check whether a request is already running, avoid duplicates, +/// and cancel all active tasks at once. The registry automatically removes +/// completed tasks via `cleanupCompleted()` to keep memory usage compact. +/// +/// Typical use cases: +/// - Ensure only one task per identifier is active at a time. +/// - Query whether a specific request is still running (`isReading`). +/// - Forcefully stop a specific request (`cancel`). +/// - Forcefully stop all tasks when leaving a screen (`cancelAll`). +actor NetworkingTasks { + private var active: [(identifier: String, task: URLSessionTask)] = [] + + /// Returns whether there is an in-flight task for the given URL. + /// + /// A task is considered in-flight if its `state` is `.running` or `.suspended`. + /// - Parameter identifier: The identifier to check. + /// - Returns: `true` if a matching in-flight task exists; otherwise `false`. + func isReading(identifier: String) -> Bool { + // Drop finished/canceling tasks globally + cleanup() + + return active.contains { + $0.identifier == identifier && ($0.task.state == .running || $0.task.state == .suspended) + } + } + + /// Tracks a newly created `URLSessionTask` for the given identifier. + /// + /// If a running entry for the same identifier exists, it is removed before appending the new one. + /// - Parameters: + /// - identifier: The identifier associated with the task. + /// - task: The `URLSessionTask` to track. + func track(identifier: String, task: URLSessionTask) { + // Drop finished/canceling tasks globally + cleanup() + + active.removeAll { + $0.identifier == identifier && $0.task.state == .running + } + active.append((identifier, task)) + nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .start, message: "Start task for identifier: \(identifier)", consoleOnly: true) + } + + /// create a Identifier + /// + func createIdentifier(account: String? = nil, path: String? = nil, name: String) -> String { + if let account, + let path { + return account + "_" + path + "_" + name + } else if let path { + return path + "_" + name + } else { + return name + } + } + + /// Cancels and removes all tasks associated with the given id. + /// + /// - Parameter identifier: The identifier whose tasks should be canceled. + func cancel(identifier: String) { + // Drop finished/canceling tasks globally + cleanup() + + for element in active where element.identifier == identifier { + element.task.cancel() + nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .cancel, message: "Cancel task for identifier: \(identifier)", consoleOnly: true) + } + active.removeAll { + $0.identifier == identifier + } + } + + /// Cancels all tracked `URLSessionTask` and clears the registry. + /// + /// Call this when leaving the page/screen or when the operation must be forcefully stopped. + func cancelAll() { + active.forEach { + $0.task.cancel() + nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .cancel, message: "Cancel task with identifier: \($0.identifier)", consoleOnly: true) + } + active.removeAll() + } + + /// Removes tasks that have completed from the registry. + /// + /// Useful to keep the in-memory list compact during long-running operations. + func cleanup() { + active.removeAll { + $0.task.state == .completed || $0.task.state == .canceling + } + } +} + +/// Quantizes per-task progress updates to integer percentages (0...100). +/// Each (serverUrlFileName) pair is tracked separately, so you get +/// at most one update per integer percent for each transfer. +actor ProgressQuantizer { + private var lastPercent: [String: Int] = [:] + + /// Returns `true` only when integer percent changes (or hits 100). + /// + /// - Parameters: + /// - serverUrlFileName: The name of the file being transferred. + /// - fraction: Progress fraction [0.0 ... 1.0]. + func shouldEmit(serverUrlFileName: String, fraction: Double) -> Bool { + let percent = min(max(Int((fraction * 100).rounded(.down)), 0), 100) + + let last = lastPercent[serverUrlFileName] ?? -1 + guard percent != last || percent == 100 else { + return false + } + + lastPercent[serverUrlFileName] = percent + return true + } + + /// Clears stored state for a finished transfer. + func clear(serverUrlFileName: String) { + lastPercent.removeValue(forKey: serverUrlFileName) + } +} + class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { static let shared = NCNetworking() @@ -94,6 +291,7 @@ class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { let downloadAvatarQueue = Queuer(name: "downloadAvatarQueue", maxConcurrentOperationCount: 10, qualityOfService: .default) let fileExistsQueue = Queuer(name: "fileExistsQueue", maxConcurrentOperationCount: 10, qualityOfService: .default) #endif + let downloadQueue = Queuer(name: "downloadQueue", maxConcurrentOperationCount: NCBrandOptions.shared.httpMaximumConnectionsPerHostInDownload, qualityOfService: .default) // MARK: - init diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index 930a965320..9121911b0b 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -271,7 +271,6 @@ actor NCNetworkingProcess { await startTimer(interval: minInterval) } } else { - // Remove upload asset await removeUploadedAssetsIfNeeded() // Set Live Photo diff --git a/iOSClient/Networking/NCService.swift b/iOSClient/Networking/NCService.swift index f6715693d3..2987e2543c 100644 --- a/iOSClient/Networking/NCService.swift +++ b/iOSClient/Networking/NCService.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2018 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCService.swift +// Nextcloud +// +// Created by Marino Faggiana on 14/03/18. +// Copyright © 2018 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import UIKit @preconcurrency import NextcloudKit diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index cd2a633acc..8be6601682 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -126,8 +126,8 @@ "_error_parameter_schema_" = "Wrong parameters, impossible to continue"; "_comments_" = "Comments"; "_sharing_" = "Sharing"; -"_details_" = "Share"; -//"_details_" = "Details"; +//"_details_" = "Share"; +"_details_" = "Details"; "_sub_details_" = "Subscription Details"; "_subscriptions_" = "Subscriptions"; "_dark_mode_" = "Dark mode"; @@ -232,8 +232,6 @@ "_settings_account_request_" = "Request account at startup"; "_print_" = "Print"; "_alias_" = "Alias"; -//"_alias_placeholder_" = "Write alias"; -//"_alias_footer_" = "Give your accounts a descriptive name such as Home, Office, School…"; "_alias_placeholder_" = "Write the alias"; "_alias_footer_" = "Give your account names a descriptive name such as Home, Office, School …"; "_chunk_size_mb_" = "Chunk size in MB"; @@ -283,7 +281,7 @@ //TOPasscodeSettingsWarningLabel.m "_1_failed_passcode_attempt" = "1 Failed Passcode Attempt"; -"_failed_passcode_attempts" = "%ld Failed Passcode Attempts"; +"_failed_passcode_attempts" = "%d Failed Passcode Attempts"; /* Background of the file listing view */ "_use_as_background_" = "Use it as a background"; @@ -324,7 +322,6 @@ "_status_message_" = "Status message"; "_status_message_placehorder_" = "What is your status?"; "_online_status_" = "Online status"; -"_clear_status_message_" = "Clear status message"; "_set_status_message_" = "Set status message"; "_clear_status_message_after_" = "Clear status after"; @@ -387,8 +384,8 @@ "_username_" = "Username"; "_change_credentials_" = "Change your credentials"; "_wifi_only_" = "Only use Wi-Fi connection"; -//"_settings_autoupload_" = "Auto upload photos"; -"_settings_autoupload_" = "Auto upload"; +"_settings_autoupload_" = "Auto upload photos"; +//"_settings_autoupload_" = "Auto upload"; "_app_version_" = "Application version"; "_app_in_use_" = "Application in use"; "_contact_by_email_" = "Contact us by email"; @@ -485,7 +482,6 @@ "_autoupload_change_location_footer_" = "Change folder used for \"Automatic upload of camera photos\" (if the option is enabled)"; "_autoupload_not_select_home_" = "Select a folder"; "_autoupload_save_album_" = "Copy photo or video into the photo album"; -"_autoupload_description_" = "Choose whether new photos or videos will be automatically uploaded to your account."; "_autoupload_fullphotos_" = "Upload the whole camera roll"; "_start_autoupload_" = "Turn on auto upload"; "_stop_autoupload_" = "Turn off auto upload"; @@ -629,11 +625,9 @@ "_add_offline_" = "Add to offline"; "_remove_passcode_" = "Remove password protection"; "_protect_passcode_" = "Protect with password"; -"_remove_favorites_" = "Unfavorite"; -"_add_favorites_" = "Favorite"; "_share_" = "Share"; "_reload_" = "Reload"; -"_open_in_" = "Open with..."; //"Open in …"; +"_open_in_" = "Open in …"; "_open_" = "Open …"; "_remove_local_file_" = "Remove locally"; "_add_local_" = "Add to local storage"; @@ -720,7 +714,7 @@ "_status_upload_error_" = "Error, waiting to upload"; "_select_media_folder_" = "Set Media folder"; "_media_viewimage_show_" = "Show only images"; -"_media_viewvideo_show_" = "Show only videos"; +"_media_viewvideo_show_" = "Show only video"; "_media_show_all_" = "Show both"; "_media_by_created_date_" = "Sort by created date"; "_media_by_upload_date_" = "Sort by upload date"; @@ -741,12 +735,10 @@ "_share_link_" = "Share link"; //"_share_link_" = "Share link"; "_share_link_" = "Link"; -//"_share_link_" = "Share link"; "_share_link_button_" = "Send link to …"; "_share_link_name_" = "Link name"; "_password_" = "Password"; "_share_password_" = "Password protected link"; -"_share_password_must_be_at_least_6_chars" = "Password must be at least 6 characters."; "_share_expirationdate_" = "Set expiration date for link"; "_date_" = "Date"; "_share_title_" = "Share"; @@ -804,42 +796,6 @@ "_new_comment_" = "New comment …"; "_edit_comment_" = "Edit comment"; "_delete_comment_" = "Delete comment"; -"_share_read_only_" = "View only"; -"_share_reshare_allowed_" = "Resharing is allowed."; -"_share_reshare_not_allowed_" = "Resharing is not allowed."; -"_sharing_message_" = "You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration."; -"_create_link_" = "Create Link"; -"personal_share_by_mail" = "Personal share by mail"; -"_your_shares_" = "Your Shares"; -"_share_linklabel_" = "Link '%@'"; -"_share_link_folder_" = "Link to folder"; -"_share_link_file_" = "Link to file"; -"no_shares_created" = "No shares created yet."; -"_advance_permissions_" = "Advanced permissions"; -"_send_new_email_" = "Send new email"; -"_apply_changes_" = "Apply changes"; -"_send_share_" = "Send share"; -"_PERMISSIONS_" = "PERMISSIONS"; -"share_editing_message" = "There are no editing functions for files unless you share them with MagentaCLOUD users."; -"_file_drop_message_" = "With File drop, only uploading is allowed. Only you can see files and folders that have been uploaded."; -"_custom_link_label" = "Your custom link label"; -"_set_password_" = "Set password"; -"_share_expiration_date_placeholder_"= "Expiration date for this share"; -"_share_download_limit_" = "Download Limit"; -"_share_download_limit_placeholder_" = "Enter download limit"; -"_share_download_limit_alert_empty_" = "Download limit cannot be empty."; -"_share_download_limit_alert_zero_" = "Download limit should be greater than 0."; -"_share_download_limit_alert_invalid_" = "Please enter a valid number for the download limit."; -"_share_remaining_download_" = "Downloads:"; -"_share_read_only_" = "View only"; -"_share_remaining_download_" = "Downloads:"; -"_share_editing_" = "Can edit"; -"_share_file_drop_" = "Filedrop only"; -"_share_hide_download_" = "Prevent download"; -//"_share_note_recipient_" = "YOUR MESSAGE"; -"_share_note_recipient_" = "Note to recipient"; -"_shareLinksearch_placeholder_" = "Contact name or email address"; -"_shareLinksearch_mail_placeholder_" = "Type a name or an email and press Enter"; "_share_reshare_allowed_" = "Resharing is allowed."; "_share_reshare_not_allowed_" = "Resharing is not allowed."; "_sharing_message_" = "You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration."; @@ -909,7 +865,6 @@ "_share_via_link_menu_password_label_" = "Password protect (%1$s)"; "_share_link_empty_exp_date_" = "You must select expiration date."; "_share_link_empty_note_message_" = "Please enter note."; -"_share_permission_should_not_be_empty_" = "Please select at least one permission."; "_remote_" = "Remote"; "_remote_group_" = "Remote group"; "_conversation_" = "Conversation"; @@ -1142,11 +1097,11 @@ "_voice_memo_stop_" = "Tap to stop"; "_voice_memo_filename_" = "Voice memo"; "_voice_memo_title_" = "Upload voice memo"; -//"Enter Passcode" = "Enter Passcode"; -//"Enter a new passcode" = "Enter a new passcode"; -//"Confirm new passcode" = "Confirm new passcode"; +"Enter Passcode" = "Enter Passcode"; +"Enter a new passcode" = "Enter a new passcode"; +"Confirm new passcode" = "Confirm new passcode"; "_too_many_failed_passcode_attempts_error_" = "Too many failed passcode attempts. Please try again."; -//"Passcodes didn't match. Try again." = "Passcodes didn't match. Try again."; +"Passcodes didn't match. Try again." = "Passcodes didn't match. Try again."; "Delete" = "Delete"; "Cancel" = "Cancel"; "_certificate_details_" = "Certificate Server details"; @@ -1205,7 +1160,7 @@ "_waiting_for_" = "Waiting for:"; "_waiting_" = "Waiting …"; "_reachable_wifi_" = "network reachable via Wi-Fi or cable"; -"_ITMS-90076_" = "Due to a change in the MagentaCLOUD application identifier, the settings and password for accessing your cloud are reset, so please re-enter your account data and check your Settings. We are sorry about that."; +"_ITMS-90076_" = "Due to a change in the Nextcloud application identifier, the settings and password for accessing your cloud are reset, so please re-enter your account data and check your Settings. We are sorry about that."; "_password_not_present_" = "Please re-insert your credentials."; "_copy_passphrase_" = "Copy passphrase"; "_ok_copy_passphrase_" = "OK and copy passphrase"; @@ -1363,7 +1318,7 @@ "_delete_in_progress_" = "Delete in progress …"; "_download_in_progress_" = "Download in progress …"; "_upload_in_progress_" = "Upload in progress …"; -"_transfer_in_progress_" = "Transfer in progress …"; +"_transfer_in_progress_" = "Transfer in progress …"; "_in_waiting_" = "In waiting"; "_in_progress_" = "In progress"; "_in_error_" = "In error"; @@ -1578,46 +1533,3 @@ You can stop it at any time, adjust the settings, and enable it again."; "_e2ee_setup_store_privatekey_" = "Private key generation returned no usable key"; "_e2ee_setup_verify_publickey_" = "Public key does not match the certificate"; "_e2ee_setup_generate_passphrase_" = "Unable to generate a passphrase"; - -// MARK: Albums -"_albums_loading_popup_desc_" = "Wait a moment..."; -"_albums_list_nav_title_" = "Albums"; -"_albums_list_new_album_btn_" = "New"; -"_albums_list_loading_msg_" = "Loading..."; -"_albums_list_error_msg_" = "Unable to load albums. Please try again later!"; -"_albums_list_album_name_validation_nonempty_" = "Album name cannot be empty."; -"_albums_list_album_name_validation_min_length_" = "Album name must be at least 3 characters."; -"_albums_list_album_name_validation_max_length_" = "Album name cannot be more than 30 characters."; -"_albums_list_album_name_validation_specials_" = "Album name cannot contain slashes."; -"_albums_list_empty_heading_" = "Create\nAlbums\nfor your\nPhotos"; -"_albums_list_empty_subheading_" = "You can organize all your photos in as many albums as you like. You haven't created an album yet."; -"_albums_list_empty_new_album_btn_" = "Create album"; -"_albums_list_own_albums_heading_" = "My albums"; -"_albums_list_entities_" = "Items"; -"_albums_list_new_album_popup_title_" = "Create new Album"; -"_albums_list_new_album_popup_desc_" = "Please enter an album name between 3 and 30 characters."; -"_albums_list_new_album_popup_hint_" = "Album's name"; -"_albums_list_new_album_popup_positive_btn_" = "Create"; -"_albums_list_new_album_popup_negative_btn_" = "Cancel"; -"_albums_list_rename_album_popup_title_" = "Rename Album"; -"_albums_list_rename_album_popup_desc_" = "Please enter new album name between 3 and 30 characters."; -"_albums_list_rename_album_popup_hint_" = "Album's new name"; -"_albums_list_rename_album_popup_positive_btn_" = "Rename"; -"_albums_list_rename_album_popup_negative_btn_" = "Cancel"; -"_albums_photo_selection_sheet_title_" = "Select items"; -"_albums_photo_selection_sheet_back_btn_" = "Back"; -"_albums_photo_selection_sheet_done_btn_" = "Done"; -"_albums_photos_loading_msg_" = "Loading photos..."; -"_albums_photos_error_msg_" = "Unable to load photos. Please try again later!"; -"_albums_photos_empty_heading_" = "All that's\nmissing are\nyour photos"; -"_albums_photos_empty_subheading_" = "You can add as many photos as you like. A photo can also belong to more than one album."; -"_albums_photos_empty_add_photos_btn_" = "Add photos"; -"_albums_photos_add_photos_btn_" = "Add"; -"_albums_photos_rename_album_btn_" = "Rename Album"; -"_albums_photos_delete_album_btn_" = "Delete Album"; -"_albums_delete_album_popup_title_" = "Delete Album?"; -"_albums_delete_album_popup_desc_" = "Are you sure you want to delete this album? This action cannot be undone."; -"_albums_delete_album_popup_positive_btn_" = "Delete"; -"_albums_delete_album_popup_negative_btn_" = "Cancel"; -"_add_to_album" = "Add to Album"; -"_remove_from_album_" = "Remove from Album"; From 25bf2bc3bc7c42ff55588629e5b09ae3215a1cd4 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Thu, 9 Apr 2026 18:59:22 +0530 Subject: [PATCH 21/40] NMC 1984 - Privacy policy customisation updated changes with 10.3.0 version --- .../Data/NCManageDatabase+LocalFile.swift | 40 +- .../Data/NCManageDatabase+Metadata.swift | 66 -- .../NCCollectionViewCommon+SyncMetadata.swift | 14 +- .../NCCollectionViewCommon.swift | 673 ++++++------------ .../NCBackgroundLocationUploadManager.swift | 19 +- iOSClient/NCGlobal.swift | 24 +- iOSClient/Networking/NCConfigServer.swift | 25 +- .../Networking/NCNetworking+Download.swift | 62 +- .../Networking/NCNetworking+LivePhoto.swift | 25 +- iOSClient/Networking/NCNetworking+Task.swift | 25 +- .../Networking/NCNetworking+Upload.swift | 21 +- .../Networking/NCNetworking+WebDAV.swift | 69 +- iOSClient/Networking/NCNetworking.swift | 198 ------ .../Networking/NCNetworkingProcess.swift | 1 + iOSClient/Networking/NCService.swift | 25 +- .../PrivacySettingsViewController.swift | 9 +- .../en.lproj/Localizable.strings | 114 ++- 17 files changed, 396 insertions(+), 1014 deletions(-) diff --git a/iOSClient/Data/NCManageDatabase+LocalFile.swift b/iOSClient/Data/NCManageDatabase+LocalFile.swift index 334e5fba25..2146dec1ad 100644 --- a/iOSClient/Data/NCManageDatabase+LocalFile.swift +++ b/iOSClient/Data/NCManageDatabase+LocalFile.swift @@ -29,17 +29,14 @@ extension NCManageDatabase { // MARK: - Realm Write + /// Adds or updates multiple local file entries corresponding to the given metadata array. + /// Uses async Realm read + single write transaction. Assumes `tableLocalFile` has a primary key. /// - Parameters: - /// - metadata: The `tableMetadata` containing file details. - /// - offline: Optional flag to mark the file as available offline. - /// - Returns: Nothing. Realm write is performed asynchronously. - func addLocalFileAsync(metadata: tableMetadata, offline: Bool? = nil) async { - // Read (non-blocking): safely detach from Realm thread - let existing: tableLocalFile? = performRealmRead { realm in - realm.objects(tableLocalFile.self) - .filter(NSPredicate(format: "ocId == %@", metadata.ocId)) - .first - .map { tableLocalFile(value: $0) } + /// - metadatas: Array of `tableMetadata` to map into `tableLocalFile`. + /// - offline: Optional override for the `offline` flag applied to all items. + func addLocalFilesAsync(metadatas: [tableMetadata], offline: Bool? = nil) async { + guard !metadatas.isEmpty else { + return } // Extract ocIds for efficient lookup @@ -62,19 +59,20 @@ extension NCManageDatabase { // Reuse existing object or create a new one let local = existingMap[metadata.ocId] ?? tableLocalFile() - addObject.account = metadata.account - addObject.etag = metadata.etag - addObject.exifDate = NSDate() - addObject.exifLatitude = "-1" - addObject.exifLongitude = "-1" - addObject.ocId = metadata.ocId - addObject.fileName = metadata.fileName + local.account = metadata.account + local.etag = metadata.etag + local.exifDate = NSDate() + local.exifLatitude = "-1" + local.exifLongitude = "-1" + local.ocId = metadata.ocId + local.fileName = metadata.fileName - if let offline { - addObject.offline = offline - } + if let offline { + local.offline = offline + } - realm.add(addObject, update: .all) + realm.add(local, update: .all) + } } } diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index 5c3b840691..67ed7ad2f2 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -151,37 +151,10 @@ extension tableMetadata { (fileNameView as NSString).deletingPathExtension } - var isRenameable: Bool { - if !NCMetadataPermissions.canRename(self) { - return false - } - if lock { - return false - } - if !isDirectoryE2EE && e2eEncrypted { - return false - } - return true - } - - var isPrintable: Bool { - if isDocumentViewableOnly { - return false - } - if ["application/pdf", "com.adobe.pdf"].contains(contentType) || contentType.hasPrefix("text/") || classFile == NKTypeClassFile.image.rawValue { - return true - } - return false - } - var isSavebleInCameraRoll: Bool { return (classFile == NKTypeClassFile.image.rawValue && contentType != "image/svg+xml") || classFile == NKTypeClassFile.video.rawValue } - var isDocumentViewableOnly: Bool { - sharePermissionsCollaborationServices == NCPermissions().permissionReadShare && classFile == NKTypeClassFile.document.rawValue - } - var isAudioOrVideo: Bool { return classFile == NKTypeClassFile.audio.rawValue || classFile == NKTypeClassFile.video.rawValue } @@ -371,17 +344,6 @@ extension tableMetadata { return !lock || (lockOwner == user && lockOwnerType == 0) } - // Return if is sharable - func isSharable() -> Bool { - guard let capabilities = NCNetworking.shared.capabilities[account] else { - return false - } - if !capabilities.fileSharingApiEnabled || (capabilities.e2EEEnabled && isDirectoryE2EE), !e2eEncrypted { - return false - } - return !e2eEncrypted - } - /// Returns a detached (unmanaged) deep copy of the current `tableMetadata` object. /// /// - Note: Primitive properties and lists of primitive values (for example `shareType`) @@ -969,34 +931,6 @@ extension NCManageDatabase { .map { $0.detachedCopy() } } ?? [] } - - func getMediaMetadatas(predicate: NSPredicate, sorted: String? = nil, ascending: Bool = false) -> ThreadSafeArray? { - - do { - let realm = try Realm() - if let sorted { - var results: [tableMetadata] = [] - switch sorted {//NCPreferences().mediaSortDate { - case "date": - results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.date as Date) > ($1.date as Date) } - case "creationDate": - results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.creationDate as Date) > ($1.creationDate as Date) } - case "uploadDate": - results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.uploadDate as Date) > ($1.uploadDate as Date) } - default: - let results = realm.objects(tableMetadata.self).filter(predicate) - return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) - } - return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) - } else { - let results = realm.objects(tableMetadata.self).filter(predicate) - return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) - } - } catch let error as NSError { -// NextcloudKit.shared.nkCommonInstance.writeLog("Could not access database: \(error)") - } - return nil - } func getMetadatas(predicate: NSPredicate, sortedByKeyPath: String, diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift index 571306d2de..5e0254858d 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift @@ -21,7 +21,7 @@ extension NCCollectionViewCommon { // If a sync task is already running, do not start a new one if let task = syncMetadatasTask, !task.isCancelled { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .info, message: "Exit: Another sync is already running. Skipping this one.", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .info, message: "Exit: Another sync is already running. Skipping this one.", consoleOnly: true) return } @@ -45,9 +45,9 @@ extension NCCollectionViewCommon { func stopSyncMetadata() async { if let task = syncMetadatasTask { if task.isCancelled { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .stop, message: "Sync Metadata for \(self.serverUrl) was already cancelled.", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .stop, message: "Sync Metadata for \(self.serverUrl) was already cancelled.", consoleOnly: true) } else { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .stop, message: "Stopping active Sync Metadata for \(self.serverUrl).", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .stop, message: "Stopping active Sync Metadata for \(self.serverUrl).", consoleOnly: true) } } @@ -74,7 +74,7 @@ extension NCCollectionViewCommon { return } let identifier = self.serverUrl + "_syncMetadata" - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .start, message: "Start Sync Metadata for \(self.serverUrl)") + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .start, message: "Start Sync Metadata for \(self.serverUrl)") // Always cancel and clear all tracked URLSessionTask on any exit path defer { @@ -85,7 +85,7 @@ extension NCCollectionViewCommon { // If a readFile for this serverUrl is already in-flight, do nothing if await networking.networkingTasks.isReading(identifier: identifier) { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .debug, message: "ReadFile for this \(self.serverUrl) is already in-flight.", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .debug, message: "ReadFile for this \(self.serverUrl) is already in-flight.", consoleOnly: true) return } @@ -151,9 +151,9 @@ extension NCCollectionViewCommon { // If this folder failed, skip it but keep processing others if resultsReadFolder.error == .success { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .network, message: "Read correctly: \(serverUrl)", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .network, message: "Read correctly: \(serverUrl)", consoleOnly: true) } else { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .error, message: "Read failed for \(serverUrl) with error: \(resultsReadFolder.error.errorDescription)") + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .error, message: "Read failed for \(serverUrl) with error: \(resultsReadFolder.error.errorDescription)") return } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 2595fb1348..f8d9b7cefe 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -4,14 +4,12 @@ import UIKit import SwiftUI -import Realm import RealmSwift import NextcloudKit import EasyTipView import LucidBanner -import MoEngageInApps -class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionFirstHeaderDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { +class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { @IBOutlet weak var collectionView: UICollectionView! @@ -52,19 +50,14 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var tabBarSelect: NCCollectionViewCommonSelectTabBar? var attributesZoomIn: UIMenuElement.Attributes = [] var attributesZoomOut: UIMenuElement.Attributes = [] - var syncMetadatasTask: Task? - let maxImageGrid: CGFloat = 7 -// var headerMenu: NCSectionFirstHeader? - var tipViewAccounts: EasyTipView? - var selectableDataSource: [RealmSwiftObject] { dataSource.getMetadataSourceForAllSections() } + var syncMetadatasTask: Task? // DECLARE var layoutKey = "" var titleCurrentFolder = "" var titlePreviusFolder: String? var enableSearchBar: Bool = false - var groupByField = "name" var headerRichWorkspaceDisable: Bool = false var emptyImageName: String? @@ -158,7 +151,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return self.serverUrl == self.utilityFileSystem.getHomeServer(session: self.session) && capabilities.recommendations } - internal let debouncer = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerReloadDataSource = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerReloadData = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerGetServerData = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerNetworkSearch = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) // MARK: - View Life Cycle @@ -241,29 +237,28 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String UserDefaults.standard.set(appVersion, forKey: "CurrentAppVersion") } - + registerForTraitChanges([UITraitUserInterfaceStyle.self]) { [weak self] (view: NCCollectionViewCommon, _) in guard let self else { return } self.sectionFirstHeader?.setRichWorkspaceColor(style: view.traitCollection.userInterfaceStyle) } - NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterChangeTheming), object: nil, queue: .main) { [weak self] _ in - guard let self else { return } - self.collectionView.reloadData() + NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterChangeTheming), object: nil, queue: .main) { _ in + let serverUrl = self.serverUrl + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: serverUrl) + } + } } - - registerForTraitChanges([UITraitUserInterfaceStyle.self]) { [weak self] (view: NCCollectionViewCommon, _) in - guard let self else { return } - self.sectionFirstHeader?.setRichWorkspaceColor(style: view.traitCollection.userInterfaceStyle) + NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterUserInteractionMonitor), object: nil, queue: .main) { _ in + Task { + await self.debouncerReloadData.resume() + } } - NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterChangeTheming), object: nil, queue: .main) { [weak self] _ in - guard let self else { return } - self.collectionView.reloadData() - } - NotificationCenter.default.addObserver(self, selector: #selector(updateIcons), name: NSNotification.Name(rawValue: global.notificationCenterUpdateIcons), object: nil) DispatchQueue.main.async { @@ -274,6 +269,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) +// AnalyticsHelper.shared.displayInAppNotification() + if titlePreviusFolder != nil { navigationController?.navigationBar.topItem?.title = titlePreviusFolder } @@ -286,6 +283,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS isEditMode = false Task { + await NCNetworking.shared.transferDispatcher.addDelegate(self) + await (self.navigationController as? NCMainNavigationController)?.setNavigationLeftItems() await (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() } @@ -311,9 +310,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - Task { - await NCNetworking.shared.transferDispatcher.addDelegate(self) - } + // Re-evaluate in-app messages after viewDidAppear + MoEngageAnalytics.shared.displayInAppNotificationSafely(reason: "viewDidAppear") NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive(_:)), name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(closeRichWorkspaceWebView), name: NSNotification.Name(rawValue: global.notificationCenterCloseRichWorkspaceWebView), object: nil) @@ -370,7 +368,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } @objc func updateIcons() { - collectionView.reloadData() +// collectionView.reloadData() + Task { + await self.reloadDataSource() + } } func isApplicationUpdated() -> Bool { @@ -386,287 +387,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.present(newViewController!, animated: true, completion: nil) } - // MARK: - Transfer Delegate - - func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } - - func transferChange(status: String, metadatasError: [tableMetadata: NKError]) { - switch status { - // DELETE - case self.global.networkingStatusDelete: - let errorForThisServer = metadatasError.first { entry in - let (key, value) = entry - return key.serverUrl == self.serverUrl && value != .success - }?.value - - let needLoadDataSource = metadatasError.contains { entry in - let (key, value) = entry - return key.serverUrl == self.serverUrl && value == .success - } - - if let error = errorForThisServer { - NCContentPresenter().showError(error: error) - } - - if self.isSearchingMode { - self.networkSearch() - } else if needLoadDataSource { - Task { - await self.reloadDataSource() - } - } else { - Task.detached { - if await self.isRecommendationActived() { - await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) - } - } - } - default: - break - } - } - - func transferChange(status: String, metadata: tableMetadata, error: NKError) { - guard session.account == metadata.account else { return } - - if error != .success { - NCContentPresenter().showError(error: error) - } - - DispatchQueue.main.async { - switch status { - // UPLOADED, UPLOADED LIVEPHOTO - case self.global.networkingStatusUploaded, self.global.networkingStatusUploadedLivePhoto: - self.debouncer.call { - if self.isSearchingMode { - self.networkSearch() - } else if self.serverUrl == metadata.serverUrl { - Task { - await self.reloadDataSource() - } - } - } - // DOWNLOAD - case self.global.networkingStatusDownloading: - Task { - if metadata.serverUrl == self.serverUrl { - await self.reloadDataSource() - } - } - case self.global.networkingStatusDownloaded: - Task { - if metadata.serverUrl == self.serverUrl { - await self.reloadDataSource() - } - } - case self.global.networkingStatusDownloadCancel: - Task { - if metadata.serverUrl == self.serverUrl { - await self.reloadDataSource() - } - } - // CREATE FOLDER - case self.global.networkingStatusCreateFolder: - if metadata.serverUrl == self.serverUrl, metadata.sessionSelector != self.global.selectorUploadAutoUpload { - self.pushMetadata(metadata) - } - // RENAME - case self.global.networkingStatusRename: - self.debouncer.call { - if self.isSearchingMode { - self.networkSearch() - } else if self.serverUrl == metadata.serverUrl { - Task { - await self.reloadDataSource() - } - } - } - // FAVORITE - case self.global.networkingStatusFavorite: - self.debouncer.call { - if self.isSearchingMode { - self.networkSearch() - } else if self is NCFavorite { - Task { - await self.reloadDataSource() - } - } else if self.serverUrl == metadata.serverUrl { - Task { - await self.reloadDataSource() - } - } - } - default: - break - } - } - } - - func transferReloadData(serverUrl: String?, status: Int?) { - self.debouncer.call { - if self.isSearchingMode { - guard status != self.global.metadataStatusWaitDelete, - status != self.global.metadataStatusWaitRename, - status != self.global.metadataStatusWaitMove, - status != self.global.metadataStatusWaitCopy, - status != self.global.metadataStatusWaitFavorite else { - return - } - self.networkSearch() - } else if ( self.serverUrl == serverUrl) || serverUrl == nil { - Task { - await self.reloadDataSource() - } - } - } - } - - func transferRequestData(serverUrl: String?) { - self.debouncer.call { - if self.isSearchingMode { - self.networkSearch() - } else if ( self.serverUrl == serverUrl) || serverUrl == nil { - Task { - await self.getServerData() - } - } - } - } - - func transferCopy(metadata: tableMetadata, destination: String, error: NKError) { - if error != .success { - NCContentPresenter().showError(error: error) - } - - if isSearchingMode { - return networkSearch() - } - - if metadata.serverUrl == self.serverUrl || destination == self.serverUrl { - Task { - await self.reloadDataSource() - } - } - } - - func transferMove(metadata: tableMetadata, destination: String, error: NKError) { - if error != .success { - NCContentPresenter().showError(error: error) - } - - if isSearchingMode { - return networkSearch() - } - - if metadata.serverUrl == self.serverUrl || destination == self.serverUrl { - Task { - await self.reloadDataSource() - } - } - } - - func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } - - func transferChange(status: String, - account: String, - fileName: String, - serverUrl: String, - selector: String?, - ocId: String, - destination: String?, - error: NKError) { - Task { - if error != .success, - error.errorCode != global.errorResourceNotFound { - await showErrorBanner(controller: self.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode) - } - guard session.account == account else { - return - } - - await self.debouncer.call { - switch status { - // UPLOADED, UPLOADED LIVEPHOTO, DELETE - case self.global.networkingStatusUploaded, - self.global.networkingStatusDelete, - self.global.networkingStatusCopyMove: - if self.isSearchingMode { - self.networkSearch() - } else if self.serverUrl == serverUrl || destination == self.serverUrl { - await self.reloadDataSource() - } - // DOWNLOAD - case self.global.networkingStatusDownloaded: - if serverUrl == self.serverUrl || self.serverUrl.isEmpty { - await self.reloadDataSource() - } - case self.global.networkingStatusDownloadCancel: - if serverUrl == self.serverUrl { - await self.reloadDataSource() - } - // CREATE FOLDER - case self.global.networkingStatusCreateFolder: - if serverUrl == self.serverUrl, - selector != self.global.selectorUploadAutoUpload, - let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { - self.pushMetadata(metadata) - } - // RENAME - case self.global.networkingStatusRename: - if self.isSearchingMode { - self.networkSearch() - } else if self.serverUrl == serverUrl { - await self.reloadDataSource() - } - // FAVORITE - case self.global.networkingStatusFavorite: - if self.isSearchingMode { - self.networkSearch() - } else if self is NCFavorite { - await self.reloadDataSource() - } else if self.serverUrl == serverUrl { - await self.reloadDataSource() - } - default: - break - } - } - } - } - - func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) { - Task { - await self.debouncer.call { - if requestData { - if self.isSearchingMode { - self.networkSearch() - } else if ( self.serverUrl == serverUrl) || serverUrl == nil { - Task { - await self.getServerData() - } - } - } else { - if self.isSearchingMode { - guard status != self.global.metadataStatusWaitDelete, - status != self.global.metadataStatusWaitRename, - status != self.global.metadataStatusWaitMove, - status != self.global.metadataStatusWaitCopy, - status != self.global.metadataStatusWaitFavorite else { - return - } - self.networkSearch() - } else if ( self.serverUrl == serverUrl) || serverUrl == nil { - Task { - await self.reloadDataSource() - } - } - } - } - } - } - // MARK: - NotificationCenter @objc func applicationWillResignActive(_ notification: NSNotification) { @@ -743,12 +463,9 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return NCBrandOptions.shared.brand } - func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } - @MainActor func startGUIGetServerData() { self.dataSource.setGetServerData(false) - self.collectionView.reloadData() // Don't show spinner on iPad root folder if UIDevice.current.userInterfaceIdiom == .pad, @@ -778,61 +495,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.navigationItem.titleView = nil self.navigationItem.title = self.titleCurrentFolder } - - func resetPlusButtonAlpha(animated: Bool = true) { } - - func isHiddenPlusButton(_ isHidden: Bool) { } - - // MARK: - Empty - - func emptyDataSetView(_ view: NCEmptyView) { - - self.emptyDataSet?.setOffset(getHeaderHeight()) - if isSearchingMode { - view.emptyImage.image = UIImage(named: "search")?.image(color: .gray, size: UIScreen.main.bounds.width) - if self.dataSourceTask?.state == .running { - view.emptyTitle.text = NSLocalizedString("_search_in_progress_", comment: "") - } else { - view.emptyTitle.text = NSLocalizedString("_search_no_record_found_", comment: "") - } - view.emptyDescription.text = NSLocalizedString("_search_instruction_", comment: "") - } else if self.dataSourceTask?.state == .running { - view.emptyImage.image = UIImage(named: "networkInProgress")?.image(color: .gray, size: UIScreen.main.bounds.width) - view.emptyTitle.text = NSLocalizedString("_request_in_progress_", comment: "") - view.emptyDescription.text = "" - } else { - if serverUrl.isEmpty { - view.emptyImage.image = emptyImage - view.emptyTitle.text = NSLocalizedString(emptyTitle, comment: "") - view.emptyDescription.text = NSLocalizedString(emptyDescription, comment: "") - } else { - view.emptyImage.image = UIImage(named: "folder_nmcloud") - view.emptyTitle.text = NSLocalizedString("_files_no_files_", comment: "") - view.emptyDescription.text = NSLocalizedString("_no_file_pull_down_", comment: "") - } - } - - let spinner = UIActivityIndicatorView(style: .medium) - spinner.startAnimating() - - let container = UIView() - container.translatesAutoresizingMaskIntoConstraints = false - container.addSubview(spinner) - - spinner.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - spinner.centerXAnchor.constraint(equalTo: container.centerXAnchor), - spinner.centerYAnchor.constraint(equalTo: container.centerYAnchor) - ]) - - self.navigationItem.titleView = container - } - - @MainActor - func restoreDefaultTitle() { - self.navigationItem.titleView = nil - self.navigationItem.title = self.titleCurrentFolder - } // MARK: - SEARCH @@ -843,7 +505,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS searchController?.searchBar.alpha = 1 } else { searchController?.searchBar.alpha = 0.3 - } } @@ -874,6 +535,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.networking.cancelUnifiedSearchFiles() self.isSearchingMode = false + self.networkSearchInProgress = false self.literalSearch = "" self.providers?.removeAll() self.dataSource.removeAll() @@ -896,11 +558,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) { guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } - - NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) -// NCDownloadAction.shared.openShare(viewController: self, metadata: metadata, page: .sharing) TealiumHelper.shared.trackEvent(title: "magentacloud-app.filebrowser.sharing", data: ["": ""]) appDelegate.adjust.trackEvent(TriggerEvent(Sharing.rawValue)) + + NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) } func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { @@ -908,30 +569,30 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS toggleMenu(metadata: metadata, image: image, sender: sender) } - func tapRichWorkspace(_ sender: Any) { - if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { - if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { - viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" - viewerRichWorkspace.serverUrl = serverUrl - viewerRichWorkspace.delegate = self - - navigationController.modalPresentationStyle = .fullScreen - self.present(navigationController, animated: true, completion: nil) - } - } - } +// func tapRichWorkspace(_ sender: Any) { +// if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { +// if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { +// viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" +// viewerRichWorkspace.serverUrl = serverUrl +// viewerRichWorkspace.delegate = self +// +// navigationController.modalPresentationStyle = .fullScreen +// self.present(navigationController, animated: true, completion: nil) +// } +// } +// } - func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { - toggleMenu(metadata: metadata, image: image, sender: sender) - } +// func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { +// toggleMenu(metadata: metadata, image: image, sender: sender) +// } - func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { - unifiedSearchMore(metadataForSection: metadataForSection) - } +// func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { +// unifiedSearchMore(metadataForSection: metadataForSection) +// } - func tapRecommendations(with metadata: tableMetadata) { - didSelectMetadata(metadata, withOcIds: false) - } +// func tapRecommendations(with metadata: tableMetadata) { +// didSelectMetadata(metadata, withOcIds: false) +// } func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } @@ -1028,7 +689,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS serverUrlFileName: serverUrlFileName) { _ in } progressHandler: { _, _, fractionCompleted in Task {@MainActor in - LucidBanner.shared.update(progress: fractionCompleted, for: token) + LucidBanner.shared.update( + payload: LucidBannerPayload.Update(progress: fractionCompleted), + for: token + ) } } @@ -1048,14 +712,12 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS fileName: fileName) Task { await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: serverUrl, requestData: true, status: nil) + delegate.transferReloadDataSource(serverUrl: self.serverUrl, requestData: true, status: nil) } } } else { - Task {@MainActor in - await showErrorBanner(scene: scene, - errorDescription: resultsUpload.error.errorDescription, - errorCode: resultsUpload.error.errorCode) + Task { + await showErrorBanner(scene: scene, text: resultsUpload.error.errorDescription) } } } @@ -1066,52 +728,21 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // MARK: - DataSource - @objc func reloadDataSource() { - - // get auto upload folder - autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() - autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: session.urlBase, userId: session.userId, account: session.account) - // get layout for view - layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) - // set GroupField for Grid - if !isSearchingMode && layoutForView?.layout == NCGlobal.shared.layoutGrid { - groupByField = "classFile" - } else { - groupByField = "name" - } - if isSearchingMode { - isDirectoryEncrypted = false - } else { - isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) - } - - DispatchQueue.main.async { -// UIView.transition(with: self.collectionView, -// duration: 0.20, -// options: .transitionCrossDissolve, -// animations: { self.collectionView.reloadData() }, -// completion: nil) -// -// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() - if isRecommendationActived { - Task.detached { + @MainActor + func reloadDataSource() async { + if !isSearchingMode { + Task.detached { + if await self.isRecommendationActived() { await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) } } } - DispatchQueue.main.async { -// UIView.transition(with: self.collectionView, -// duration: 0.20, -// options: .transitionCrossDissolve, -// animations: { self.collectionView.reloadData() }, -// completion: nil) -// -// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() - self.refreshControl.endRefreshing() - self.collectionView.reloadData() - self.setNavigationRightItems() + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: self.serverUrl) } + + await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() } func getServerData(forced: Bool = false) async { } @@ -1165,8 +796,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } completion: { metadatasSearch, error in Task { guard let metadatasSearch, - error == .success, - self.isSearchingMode + error == .success, + self.isSearchingMode else { self.networkSearchInProgress = false await self.reloadDataSource() @@ -1193,7 +824,11 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS guard let metadataForSection = metadataForSection, let lastSearchResult = metadataForSection.lastSearchResult, let cursor = lastSearchResult.cursor, let term = literalSearch else { return } metadataForSection.unifiedSearchInProgress = true - self.collectionView?.reloadData() + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: nil) + } + } self.networking.unifiedSearchFilesProvider(id: lastSearchResult.id, term: term, limit: 5, cursor: cursor, account: session.account) { task in self.searchDataSourceTask = task @@ -1202,12 +837,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } completion: { _, searchResult, metadatas, error in if error != .success { - Task {@MainActor in - await showErrorBanner( - controller: self.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode - ) + Task { + await showErrorBanner(controller: self.controller, text: error.errorDescription) } } @@ -1215,8 +846,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS guard let searchResult = searchResult, let metadatas = metadatas else { return } self.dataSource.appendMetadatasToSection(metadatas, metadataForSection: metadataForSection, lastSearchResult: searchResult) - DispatchQueue.main.async { - self.collectionView?.reloadData() + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: nil) + } } } } @@ -1249,21 +882,38 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } - func pushViewController(viewController: UIViewController) { - if pushed { return } + // MARK: - Header size - pushed = true - navigationController?.pushViewController(viewController, animated: true) - } + func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, + heightHeaderRecommendations: CGFloat, + heightHeaderSection: CGFloat) { + var heightHeaderRichWorkspace: CGFloat = 0 + var heightHeaderRecommendations: CGFloat = 0 + var heightHeaderSection: CGFloat = 0 + + if showDescription, + !isSearchingMode, + let richWorkspaceText = self.richWorkspaceText, + !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { + heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 + } - // MARK: - Header size - - func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { - if headerMenuTransferView, - NCNetworking.shared.isOnline, - let results = database.getResultsMetadatas(predicate: NSPredicate(format: "status IN %@", [global.metadataStatusWaitUpload, global.metadataStatusUploading])), - !results.isEmpty { - return Array(results) + if isRecommendationActived, + !isSearchingMode, + NCPreferences().showRecommendedFiles, + !self.database.getRecommendedFiles(account: self.session.account).isEmpty { + heightHeaderRecommendations = self.heightHeaderRecommendations + heightHeaderSection = self.heightHeaderSection + } + + if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { + if section == 0 { + return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) + } else { + return (0, 0, self.heightHeaderSection) + } + } else { + return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) } } @@ -1308,4 +958,105 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return CGSize(width: collectionView.frame.width, height: 0) } } + + func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } +} + +extension NCCollectionViewCommon: NCSectionFirstHeaderDelegate { + func tapRichWorkspace(_ sender: Any) { + if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { + if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { + viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" + viewerRichWorkspace.serverUrl = serverUrl + viewerRichWorkspace.delegate = self + + navigationController.modalPresentationStyle = .fullScreen + self.present(navigationController, animated: true, completion: nil) + } + } + } + + func tapRecommendations(with metadata: tableMetadata) { + didSelectMetadata(metadata, withOcIds: false) + } +} + +extension NCCollectionViewCommon: NCSectionFooterDelegate { + func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { + unifiedSearchMore(metadataForSection: metadataForSection) + } +} + +extension NCCollectionViewCommon: NCTransferDelegate { + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + + func transferReloadData(serverUrl: String?) { + Task { + await self.debouncerReloadData.call({ + self.collectionView.reloadData() + }, immediate: true) + } + } + + func transferChange(status: String, + account: String, + fileName: String, + serverUrl: String, + selector: String?, + ocId: String, + destination: String?, + error: NKError) { + Task { + if error != .success, + error.errorCode != global.errorResourceNotFound { + await showErrorBanner(controller: self.controller, text: error.errorDescription) + } + guard session.account == account else { + return + } + + if status == self.global.networkingStatusCreateFolder { + if serverUrl == self.serverUrl, + selector != self.global.selectorUploadAutoUpload, + let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { + self.pushMetadata(metadata) + } + return + } + + if self.isSearchingMode { + await self.debouncerNetworkSearch.call { + self.networkSearch() + } + } else if self.serverUrl == serverUrl || destination == self.serverUrl || self.serverUrl.isEmpty { + await self.debouncerReloadDataSource.call { + await self.reloadDataSource() + } + } + } + } + + func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { + Task { + if self.isSearchingMode { + await self.debouncerNetworkSearch.call { + self.networkSearch() + } + return + } + + if requestData && (self.serverUrl == serverUrl || serverUrl == nil) { + await self.debouncerGetServerData.call { + await self.getServerData() + } + return + } + + if self.serverUrl == serverUrl || serverUrl == nil { + await self.debouncerReloadDataSource.call { + await self.reloadDataSource() + } + } + } + } } diff --git a/iOSClient/NCBackgroundLocationUploadManager.swift b/iOSClient/NCBackgroundLocationUploadManager.swift index 8f63b82e9d..154fda662d 100644 --- a/iOSClient/NCBackgroundLocationUploadManager.swift +++ b/iOSClient/NCBackgroundLocationUploadManager.swift @@ -1,10 +1,6 @@ -// -// NCBackgroundLocationUploadManager.swift -// Nextcloud -// -// Created by Marino Faggiana on 06/06/25. -// Copyright © 2025 Marino Faggiana. All rights reserved. -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import CoreLocation import NextcloudKit @@ -106,15 +102,16 @@ class NCBackgroundLocationUploadManager: NSObject, CLLocationManagerDelegate { } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + // Must work only in background guard isAppInBackground else { return } // Open Realm - if database.openRealmBackground() { - let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! - let location = locations.last - nkLog(tag: self.global.logTagLocation, emoji: .start, message: "Triggered by location change: \(location?.coordinate.latitude ?? 0), \(location?.coordinate.longitude ?? 0)") + guard NCManageDatabase.shared.openRealmBackground() else { + nkLog(tag: self.global.logTagLocation, emoji: .error, message: "Failed to open Realm in Location Manager") + return + } let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! let location = locations.last diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index c7c11d0a81..2fcf152ceb 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -50,7 +50,7 @@ final class NCGlobal: Sendable { // let avatarSize: Int = 128 * Int(UIScreen.main.scale) let avatarSizeRounded: Int = 128 - + // Preview size // let size1024: CGSize = CGSize(width: 1024, height: 1024) @@ -104,6 +104,7 @@ final class NCGlobal: Sendable { let layoutViewOffline = "LayoutOffline" let layoutViewFavorite = "LayoutFavorite" let layoutViewFiles = "LayoutFiles" + let layoutViewTransfers = "LayoutTransfers" let layoutViewRecent = "LayoutRecent" let layoutViewShares = "LayoutShares" let layoutViewShareExtension = "LayoutShareExtension" @@ -125,7 +126,6 @@ final class NCGlobal: Sendable { let heightFooterButton: CGFloat = 30 let endHeightFooter: CGFloat = 85 - // Text - OnlyOffice - Collabora - QuickLook // let editorText = "text" @@ -229,7 +229,7 @@ final class NCGlobal: Sendable { let selectorSynchronizationOffline = "synchronizationOffline" let selectorPrint = "print" let selectorDeleteFile = "deleteFile" - + // Metadata : Status // // 0 normal @@ -259,6 +259,7 @@ final class NCGlobal: Sendable { let metadataStatusForScreenAwake = [-1, -2, 1, 2] let metadataStatusHideInView = [1, 2, 3, 11] let metadataStatusWaitWebDav = [10, 11, 12, 13, 14, 15] + let metadataStatusTransfers = [-2, -3, 2, 3, 10, 11, 12, 13, 14, 15] let metadatasStatusInWaiting = [-1, 1, 10, 11, 12, 13, 14, 15] let metadatasStatusInProgress = [-2, 2] @@ -275,16 +276,22 @@ final class NCGlobal: Sendable { let notificationCenterChangeTheming = "changeTheming" // userInfo: account let notificationCenterRichdocumentGrabFocus = "richdocumentGrabFocus" let notificationCenterReloadDataNCShare = "reloadDataNCShare" - let notificationCenterDidCreateShareLink = "didCreateShareLink" - let notificationCenterCloseRichWorkspaceWebView = "closeRichWorkspaceWebView" let notificationCenterReloadAvatar = "reloadAvatar" let notificationCenterClearCache = "clearCache" let notificationCenterCheckUserDelaultErrorDone = "checkUserDelaultErrorDone" // userInfo: account, controller let notificationCenterServerDidUpdate = "serverDidUpdate" // userInfo: account let notificationCenterNetworkReachability = "networkReachability" + let notificationCenterDidCreateShareLink = "didCreateShareLink" + let notificationCenterDeleteFile = "deleteFile" // userInfo: [ocId], error + let notificationCenterCopyMoveFile = "copyMoveFile" // userInfo: [ocId] serverUrl, account, dragdrop, type (copy, move) + let notificationCenterMoveFile = "moveFile" // userInfo: [ocId], [indexPath], error + let notificationCenterCopyFile = "copyFile" // userInfo: [ocId], [indexPath], error let notificationCenterRenameFile = "renameFile" // userInfo: serverUrl, account, error + let notificationCenterFavoriteFile = "favoriteFile" // userInfo: ocId, serverUrl + let notificationCenterFileExists = "fileExists" // userInfo: ocId, fileExists + let notificationCenterReloadDataSource = "reloadDataSource" // userInfo: serverUrl?, clearDataSource let notificationCenterMenuSearchTextPDF = "menuSearchTextPDF" let notificationCenterMenuGotToPageInPDF = "menuGotToPageInPDF" @@ -292,6 +299,7 @@ final class NCGlobal: Sendable { let notificationCenterOpenMediaDetail = "openMediaDetail" // userInfo: ocId let notificationCenterDismissScanDocument = "dismissScanDocument" + let notificationCenterDismissUploadAssets = "dismissUploadAssets" let notificationCenterEnableSwipeGesture = "enableSwipeGesture" let notificationCenterDisableSwipeGesture = "disableSwipeGesture" @@ -416,7 +424,7 @@ final class NCGlobal: Sendable { let keyFileNameAutoUploadType = "fileNameAutoUploadType" let keyFileNameOriginal = "fileNameOriginal" let keyFileNameOriginalAutoUpload = "fileNameOriginalAutoUpload" - + // LOG TAG // let logTagTask = "BGT" @@ -435,6 +443,10 @@ final class NCGlobal: Sendable { // let udMigrationMultiDomains = "migrationMultiDomains" let udLastVersion = "lastVersion" + + // Album + // + let selectedTabIndexAlbum: Int = 3 } /** diff --git a/iOSClient/Networking/NCConfigServer.swift b/iOSClient/Networking/NCConfigServer.swift index d5e4d2a867..f82b466647 100644 --- a/iOSClient/Networking/NCConfigServer.swift +++ b/iOSClient/Networking/NCConfigServer.swift @@ -1,25 +1,6 @@ -// -// NCConfigServer.swift -// Nextcloud -// -// Created by Marino Faggiana on 05/12/22. -// Copyright © 2022 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2022 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import UIKit diff --git a/iOSClient/Networking/NCNetworking+Download.swift b/iOSClient/Networking/NCNetworking+Download.swift index 22ecb91653..4d9fe0c51c 100644 --- a/iOSClient/Networking/NCNetworking+Download.swift +++ b/iOSClient/Networking/NCNetworking+Download.swift @@ -1,25 +1,6 @@ -// -// NCNetworking+Download.swift -// Nextcloud -// -// Created by Marino Faggiana on 07/02/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit import NextcloudKit @@ -346,40 +327,3 @@ extension NCNetworking { } } } - -class NCOperationDownload: ConcurrentOperation, @unchecked Sendable { - var metadata: tableMetadata - var selector: String - - init(metadata: tableMetadata, selector: String) { - self.metadata = tableMetadata.init(value: metadata) - self.selector = selector - } - - override func start() { - guard !isCancelled else { return self.finish() } - - metadata.session = NCNetworking.shared.sessionDownload - metadata.sessionError = "" - metadata.sessionSelector = selector - metadata.sessionTaskIdentifier = 0 - metadata.status = NCGlobal.shared.metadataStatusWaitDownload - -// let metadata = NCManageDatabase.shared.addMetadata(metadata) - -// NCNetworking.shared.download(metadata: metadata, withNotificationProgressTask: true) { -// } completion: { _, _ in -// self.finish() -// } - Task { - await download(withSelector: self.selector) - } - } - - private func download(withSelector selector: String = "") async { - await NCNetworking.shared.downloadFile(metadata: metadata) { _ in - self.finish() - } taskHandler: { _ in } - - } -} diff --git a/iOSClient/Networking/NCNetworking+LivePhoto.swift b/iOSClient/Networking/NCNetworking+LivePhoto.swift index 072b3b087f..e861b76b81 100644 --- a/iOSClient/Networking/NCNetworking+LivePhoto.swift +++ b/iOSClient/Networking/NCNetworking+LivePhoto.swift @@ -1,25 +1,6 @@ -// -// NCNetworking+LivePhoto.swift -// Nextcloud -// -// Created by Marino Faggiana on 07/02/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit import NextcloudKit diff --git a/iOSClient/Networking/NCNetworking+Task.swift b/iOSClient/Networking/NCNetworking+Task.swift index 1b6122252b..f28830b583 100644 --- a/iOSClient/Networking/NCNetworking+Task.swift +++ b/iOSClient/Networking/NCNetworking+Task.swift @@ -1,25 +1,6 @@ -// -// NCNetworking+Task.swift -// Nextcloud -// -// Created by Marino Faggiana on 24/08/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import UIKit diff --git a/iOSClient/Networking/NCNetworking+Upload.swift b/iOSClient/Networking/NCNetworking+Upload.swift index 6c985dbe7c..9467e9c0b7 100644 --- a/iOSClient/Networking/NCNetworking+Upload.swift +++ b/iOSClient/Networking/NCNetworking+Upload.swift @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + import UIKit import NextcloudKit import Alamofire @@ -41,22 +45,6 @@ extension NCNetworking { } taskHandler(task) } progressHandler: { progress in - Task { - guard await self.progressQuantizer.shouldEmit(serverUrlFileName: serverUrlFileName, fraction: progress.fractionCompleted) else { - return - } - - if let metadata { - await NCManageDatabase.shared.setMetadataProgress(ocId: metadata.ocId, progress: progress.fractionCompleted) - await self.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferProgressDidUpdate(progress: Float(progress.fractionCompleted), - totalBytes: progress.totalUnitCount, - totalBytesExpected: progress.completedUnitCount, - fileName: metadata.fileName, - serverUrl: metadata.serverUrl) - } - } - } progressHandler(progress.completedUnitCount, progress.totalUnitCount, progress.fractionCompleted) } @@ -390,7 +378,6 @@ extension NCNetworking { error: self.global.diagnosticProblemsUploadServerError) } } - await NCManageDatabase.shared.updateBadge() } // MARK: - diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 4d78a1844e..37629dadd1 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -1,25 +1,6 @@ -// -// NCNetworking+WebDAV.swift -// Nextcloud -// -// Created by Marino Faggiana on 07/02/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit import NextcloudKit @@ -484,50 +465,6 @@ extension NCNetworking { return .success } - - func renameMetadata(_ metadata: tableMetadata, - fileNameNew: String, - indexPath: IndexPath, - viewController: UIViewController?, - completion: @escaping (_ error: NKError) -> Void) { - - let permission = NCMetadataPermissions.permissionsContainsString(metadata.permissions, permissions: NCMetadataPermissions.permissionCanRename) - if (!metadata.permissions.isEmpty && permission == false) || - (metadata.status != global.metadataStatusNormal && metadata.status != global.metadataStatusWaitRename) { -// return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_no_permission_modify_file_")) -// let error = await NCNetworkingE2EERename().rename(metadata: metadata, fileNameNew: fileNameNew) -// DispatchQueue.main.async { completion(error) } - completion(NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_no_permission_modify_file_")) - } - - if metadata.isDirectoryE2EE { -#if !EXTENSION - if isOffline { -// return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) - completion(NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) - } - Task { - let error = await NCNetworkingE2EERename().rename(metadata: metadata, fileNameNew: fileNameNew) - if error != .success { -// NCContentPresenter().showError(error: error) - completion(error) - } - } -#endif - } else { - Task { - await self.transferDispatcher.notifyAllDelegatesAsync { delegate in - let status = self.global.metadataStatusWaitRename - await NCManageDatabase.shared.renameMetadata(fileNameNew: fileNameNew, ocId: metadata.ocId, status: status) - delegate.transferReloadData(serverUrl: metadata.serverUrl, status: status) - } - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterRenameFile, userInfo: ["serverUrl": metadata.serverUrl, "account": metadata.account, "error": NKError(errorCode: 0, errorDescription: ""), "ocId": metadata.ocId, "indexPath": indexPath]) - - } - - completion(NKError(errorCode: 0, errorDescription: "")) - } - } func renameFileOrFolder(metadata: tableMetadata) async -> NKError { let serverUrlFileNameSource = metadata.serverUrlFileName diff --git a/iOSClient/Networking/NCNetworking.swift b/iOSClient/Networking/NCNetworking.swift index 0c86cf84c8..c0684756d4 100644 --- a/iOSClient/Networking/NCNetworking.swift +++ b/iOSClient/Networking/NCNetworking.swift @@ -37,203 +37,6 @@ protocol NCTransferDelegate: AnyObject { serverUrl: String) } -extension NCTransferDelegate { - func transferChange(status: String, - account: String, - fileName: String, - serverUrl: String, - selector: String?, - ocId: String, - destination: String?, - error: NKError) {} - func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) {} - func transferProgressDidUpdate(progress: Float, - totalBytes: Int64, - totalBytesExpected: Int64, - fileName: String, - serverUrl: String) {} -} - -/// Actor-based delegate dispatcher using weak references. -actor NCTransferDelegateDispatcher { - // Weak reference collection of delegates - private var transferDelegates = NSHashTable.weakObjects() - - /// Adds a delegate safely. - func addDelegate(_ delegate: NCTransferDelegate) { - transferDelegates.add(delegate) - } - - /// Remove a delegate safely. - func removeDelegate(_ delegate: NCTransferDelegate) { - transferDelegates.remove(delegate) - } - - /// Notifies all delegates. - func notifyAllDelegates(_ block: (NCTransferDelegate) -> Void) { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - block(delegate) - } - } - - func notifyAllDelegatesAsync(_ block: @escaping (NCTransferDelegate) async -> Void) async { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - await block(delegate) - } - } - - /// Notifies the delegate for a specific scene. - func notifyDelegate(forScene sceneIdentifier: String, _ block: (NCTransferDelegate) -> Void) { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - if delegate.sceneIdentifier == sceneIdentifier { - block(delegate) - } - } - } - - /// Notifies matching and non-matching delegates for a specific scene. - func notifyDelegates(forScene sceneIdentifier: String, - matching: (NCTransferDelegate) -> Void, - others: (NCTransferDelegate) -> Void) { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - if delegate.sceneIdentifier == sceneIdentifier { - matching(delegate) - } else { - others(delegate) - } - } - } -} - -/// A thread-safe registry for tracking in-flight `URLSessionTask` instances. -/// -/// Each task is associated with a string identifier (`identifier`) that you define, -/// allowing you to check whether a request is already running, avoid duplicates, -/// and cancel all active tasks at once. The registry automatically removes -/// completed tasks via `cleanupCompleted()` to keep memory usage compact. -/// -/// Typical use cases: -/// - Ensure only one task per identifier is active at a time. -/// - Query whether a specific request is still running (`isReading`). -/// - Forcefully stop a specific request (`cancel`). -/// - Forcefully stop all tasks when leaving a screen (`cancelAll`). -actor NetworkingTasks { - private var active: [(identifier: String, task: URLSessionTask)] = [] - - /// Returns whether there is an in-flight task for the given URL. - /// - /// A task is considered in-flight if its `state` is `.running` or `.suspended`. - /// - Parameter identifier: The identifier to check. - /// - Returns: `true` if a matching in-flight task exists; otherwise `false`. - func isReading(identifier: String) -> Bool { - // Drop finished/canceling tasks globally - cleanup() - - return active.contains { - $0.identifier == identifier && ($0.task.state == .running || $0.task.state == .suspended) - } - } - - /// Tracks a newly created `URLSessionTask` for the given identifier. - /// - /// If a running entry for the same identifier exists, it is removed before appending the new one. - /// - Parameters: - /// - identifier: The identifier associated with the task. - /// - task: The `URLSessionTask` to track. - func track(identifier: String, task: URLSessionTask) { - // Drop finished/canceling tasks globally - cleanup() - - active.removeAll { - $0.identifier == identifier && $0.task.state == .running - } - active.append((identifier, task)) - nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .start, message: "Start task for identifier: \(identifier)", consoleOnly: true) - } - - /// create a Identifier - /// - func createIdentifier(account: String? = nil, path: String? = nil, name: String) -> String { - if let account, - let path { - return account + "_" + path + "_" + name - } else if let path { - return path + "_" + name - } else { - return name - } - } - - /// Cancels and removes all tasks associated with the given id. - /// - /// - Parameter identifier: The identifier whose tasks should be canceled. - func cancel(identifier: String) { - // Drop finished/canceling tasks globally - cleanup() - - for element in active where element.identifier == identifier { - element.task.cancel() - nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .cancel, message: "Cancel task for identifier: \(identifier)", consoleOnly: true) - } - active.removeAll { - $0.identifier == identifier - } - } - - /// Cancels all tracked `URLSessionTask` and clears the registry. - /// - /// Call this when leaving the page/screen or when the operation must be forcefully stopped. - func cancelAll() { - active.forEach { - $0.task.cancel() - nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .cancel, message: "Cancel task with identifier: \($0.identifier)", consoleOnly: true) - } - active.removeAll() - } - - /// Removes tasks that have completed from the registry. - /// - /// Useful to keep the in-memory list compact during long-running operations. - func cleanup() { - active.removeAll { - $0.task.state == .completed || $0.task.state == .canceling - } - } -} - -/// Quantizes per-task progress updates to integer percentages (0...100). -/// Each (serverUrlFileName) pair is tracked separately, so you get -/// at most one update per integer percent for each transfer. -actor ProgressQuantizer { - private var lastPercent: [String: Int] = [:] - - /// Returns `true` only when integer percent changes (or hits 100). - /// - /// - Parameters: - /// - serverUrlFileName: The name of the file being transferred. - /// - fraction: Progress fraction [0.0 ... 1.0]. - func shouldEmit(serverUrlFileName: String, fraction: Double) -> Bool { - let percent = min(max(Int((fraction * 100).rounded(.down)), 0), 100) - - let last = lastPercent[serverUrlFileName] ?? -1 - guard percent != last || percent == 100 else { - return false - } - - lastPercent[serverUrlFileName] = percent - return true - } - - /// Clears stored state for a finished transfer. - func clear(serverUrlFileName: String) { - lastPercent.removeValue(forKey: serverUrlFileName) - } -} - class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { static let shared = NCNetworking() @@ -291,7 +94,6 @@ class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { let downloadAvatarQueue = Queuer(name: "downloadAvatarQueue", maxConcurrentOperationCount: 10, qualityOfService: .default) let fileExistsQueue = Queuer(name: "fileExistsQueue", maxConcurrentOperationCount: 10, qualityOfService: .default) #endif - let downloadQueue = Queuer(name: "downloadQueue", maxConcurrentOperationCount: NCBrandOptions.shared.httpMaximumConnectionsPerHostInDownload, qualityOfService: .default) // MARK: - init diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index 9121911b0b..930a965320 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -271,6 +271,7 @@ actor NCNetworkingProcess { await startTimer(interval: minInterval) } } else { + // Remove upload asset await removeUploadedAssetsIfNeeded() // Set Live Photo diff --git a/iOSClient/Networking/NCService.swift b/iOSClient/Networking/NCService.swift index 2987e2543c..f6715693d3 100644 --- a/iOSClient/Networking/NCService.swift +++ b/iOSClient/Networking/NCService.swift @@ -1,25 +1,6 @@ -// -// NCService.swift -// Nextcloud -// -// Created by Marino Faggiana on 14/03/18. -// Copyright © 2018 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2018 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit @preconcurrency import NextcloudKit diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index b3993620ba..757d5532d5 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -13,7 +13,7 @@ import XLForm class PrivacySettingsViewController: XLFormViewController{ - @objc public var isShowSettingsButton: Bool = false + @objc public var isShowSettingsButton: Bool = false override func viewDidLoad() { super.viewDidLoad() @@ -36,6 +36,8 @@ class PrivacySettingsViewController: XLFormViewController{ initializeForm() } + + //MARK: XLForm func initializeForm() { @@ -56,6 +58,7 @@ class PrivacySettingsViewController: XLFormViewController{ section.footerTitle = NSLocalizedString("_privacy_settings_help_text_", comment: "") form.addFormSection(section) + //custom cell section = XLFormSectionDescriptor.formSection(withTitle: "") section.footerTitle = NSLocalizedString("_required_data_collection_help_text_", comment: "") @@ -64,6 +67,7 @@ class PrivacySettingsViewController: XLFormViewController{ XLFormViewController.cellClassesForRowDescriptorTypes()["RequiredDataCollectionCustomCellType"] = RequiredDataCollectionSwitch.self + row = XLFormRowDescriptor(tag: "ButtonDestinationFolder", rowType: "RequiredDataCollectionCustomCellType", title: "") row.cellConfig["requiredDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue @@ -78,6 +82,7 @@ class PrivacySettingsViewController: XLFormViewController{ XLFormViewController.cellClassesForRowDescriptorTypes()["AnalysisDataCollectionCustomCellType"] = AnalysisDataCollectionSwitch.self + row = XLFormRowDescriptor(tag: "AnalysisDataCollectionSwitch", rowType: "AnalysisDataCollectionCustomCellType", title: "") row.cellConfig["analysisDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue @@ -92,11 +97,13 @@ class PrivacySettingsViewController: XLFormViewController{ section.addFormRow(row) + XLFormViewController.cellClassesForRowDescriptorTypes()["SaveSettingsButton"] = SaveSettingsCustomButtonCell.self section = XLFormSectionDescriptor.formSection(withTitle: "") form.addFormSection(section) + row = XLFormRowDescriptor(tag: "SaveSettingsButton", rowType: "SaveSettingsButton", title: "") row.cellConfig["backgroundColor"] = UIColor.clear diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 8be6601682..cd2a633acc 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -126,8 +126,8 @@ "_error_parameter_schema_" = "Wrong parameters, impossible to continue"; "_comments_" = "Comments"; "_sharing_" = "Sharing"; -//"_details_" = "Share"; -"_details_" = "Details"; +"_details_" = "Share"; +//"_details_" = "Details"; "_sub_details_" = "Subscription Details"; "_subscriptions_" = "Subscriptions"; "_dark_mode_" = "Dark mode"; @@ -232,6 +232,8 @@ "_settings_account_request_" = "Request account at startup"; "_print_" = "Print"; "_alias_" = "Alias"; +//"_alias_placeholder_" = "Write alias"; +//"_alias_footer_" = "Give your accounts a descriptive name such as Home, Office, School…"; "_alias_placeholder_" = "Write the alias"; "_alias_footer_" = "Give your account names a descriptive name such as Home, Office, School …"; "_chunk_size_mb_" = "Chunk size in MB"; @@ -281,7 +283,7 @@ //TOPasscodeSettingsWarningLabel.m "_1_failed_passcode_attempt" = "1 Failed Passcode Attempt"; -"_failed_passcode_attempts" = "%d Failed Passcode Attempts"; +"_failed_passcode_attempts" = "%ld Failed Passcode Attempts"; /* Background of the file listing view */ "_use_as_background_" = "Use it as a background"; @@ -322,6 +324,7 @@ "_status_message_" = "Status message"; "_status_message_placehorder_" = "What is your status?"; "_online_status_" = "Online status"; +"_clear_status_message_" = "Clear status message"; "_set_status_message_" = "Set status message"; "_clear_status_message_after_" = "Clear status after"; @@ -384,8 +387,8 @@ "_username_" = "Username"; "_change_credentials_" = "Change your credentials"; "_wifi_only_" = "Only use Wi-Fi connection"; -"_settings_autoupload_" = "Auto upload photos"; -//"_settings_autoupload_" = "Auto upload"; +//"_settings_autoupload_" = "Auto upload photos"; +"_settings_autoupload_" = "Auto upload"; "_app_version_" = "Application version"; "_app_in_use_" = "Application in use"; "_contact_by_email_" = "Contact us by email"; @@ -482,6 +485,7 @@ "_autoupload_change_location_footer_" = "Change folder used for \"Automatic upload of camera photos\" (if the option is enabled)"; "_autoupload_not_select_home_" = "Select a folder"; "_autoupload_save_album_" = "Copy photo or video into the photo album"; +"_autoupload_description_" = "Choose whether new photos or videos will be automatically uploaded to your account."; "_autoupload_fullphotos_" = "Upload the whole camera roll"; "_start_autoupload_" = "Turn on auto upload"; "_stop_autoupload_" = "Turn off auto upload"; @@ -625,9 +629,11 @@ "_add_offline_" = "Add to offline"; "_remove_passcode_" = "Remove password protection"; "_protect_passcode_" = "Protect with password"; +"_remove_favorites_" = "Unfavorite"; +"_add_favorites_" = "Favorite"; "_share_" = "Share"; "_reload_" = "Reload"; -"_open_in_" = "Open in …"; +"_open_in_" = "Open with..."; //"Open in …"; "_open_" = "Open …"; "_remove_local_file_" = "Remove locally"; "_add_local_" = "Add to local storage"; @@ -714,7 +720,7 @@ "_status_upload_error_" = "Error, waiting to upload"; "_select_media_folder_" = "Set Media folder"; "_media_viewimage_show_" = "Show only images"; -"_media_viewvideo_show_" = "Show only video"; +"_media_viewvideo_show_" = "Show only videos"; "_media_show_all_" = "Show both"; "_media_by_created_date_" = "Sort by created date"; "_media_by_upload_date_" = "Sort by upload date"; @@ -735,10 +741,12 @@ "_share_link_" = "Share link"; //"_share_link_" = "Share link"; "_share_link_" = "Link"; +//"_share_link_" = "Share link"; "_share_link_button_" = "Send link to …"; "_share_link_name_" = "Link name"; "_password_" = "Password"; "_share_password_" = "Password protected link"; +"_share_password_must_be_at_least_6_chars" = "Password must be at least 6 characters."; "_share_expirationdate_" = "Set expiration date for link"; "_date_" = "Date"; "_share_title_" = "Share"; @@ -796,6 +804,42 @@ "_new_comment_" = "New comment …"; "_edit_comment_" = "Edit comment"; "_delete_comment_" = "Delete comment"; +"_share_read_only_" = "View only"; +"_share_reshare_allowed_" = "Resharing is allowed."; +"_share_reshare_not_allowed_" = "Resharing is not allowed."; +"_sharing_message_" = "You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration."; +"_create_link_" = "Create Link"; +"personal_share_by_mail" = "Personal share by mail"; +"_your_shares_" = "Your Shares"; +"_share_linklabel_" = "Link '%@'"; +"_share_link_folder_" = "Link to folder"; +"_share_link_file_" = "Link to file"; +"no_shares_created" = "No shares created yet."; +"_advance_permissions_" = "Advanced permissions"; +"_send_new_email_" = "Send new email"; +"_apply_changes_" = "Apply changes"; +"_send_share_" = "Send share"; +"_PERMISSIONS_" = "PERMISSIONS"; +"share_editing_message" = "There are no editing functions for files unless you share them with MagentaCLOUD users."; +"_file_drop_message_" = "With File drop, only uploading is allowed. Only you can see files and folders that have been uploaded."; +"_custom_link_label" = "Your custom link label"; +"_set_password_" = "Set password"; +"_share_expiration_date_placeholder_"= "Expiration date for this share"; +"_share_download_limit_" = "Download Limit"; +"_share_download_limit_placeholder_" = "Enter download limit"; +"_share_download_limit_alert_empty_" = "Download limit cannot be empty."; +"_share_download_limit_alert_zero_" = "Download limit should be greater than 0."; +"_share_download_limit_alert_invalid_" = "Please enter a valid number for the download limit."; +"_share_remaining_download_" = "Downloads:"; +"_share_read_only_" = "View only"; +"_share_remaining_download_" = "Downloads:"; +"_share_editing_" = "Can edit"; +"_share_file_drop_" = "Filedrop only"; +"_share_hide_download_" = "Prevent download"; +//"_share_note_recipient_" = "YOUR MESSAGE"; +"_share_note_recipient_" = "Note to recipient"; +"_shareLinksearch_placeholder_" = "Contact name or email address"; +"_shareLinksearch_mail_placeholder_" = "Type a name or an email and press Enter"; "_share_reshare_allowed_" = "Resharing is allowed."; "_share_reshare_not_allowed_" = "Resharing is not allowed."; "_sharing_message_" = "You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration."; @@ -865,6 +909,7 @@ "_share_via_link_menu_password_label_" = "Password protect (%1$s)"; "_share_link_empty_exp_date_" = "You must select expiration date."; "_share_link_empty_note_message_" = "Please enter note."; +"_share_permission_should_not_be_empty_" = "Please select at least one permission."; "_remote_" = "Remote"; "_remote_group_" = "Remote group"; "_conversation_" = "Conversation"; @@ -1097,11 +1142,11 @@ "_voice_memo_stop_" = "Tap to stop"; "_voice_memo_filename_" = "Voice memo"; "_voice_memo_title_" = "Upload voice memo"; -"Enter Passcode" = "Enter Passcode"; -"Enter a new passcode" = "Enter a new passcode"; -"Confirm new passcode" = "Confirm new passcode"; +//"Enter Passcode" = "Enter Passcode"; +//"Enter a new passcode" = "Enter a new passcode"; +//"Confirm new passcode" = "Confirm new passcode"; "_too_many_failed_passcode_attempts_error_" = "Too many failed passcode attempts. Please try again."; -"Passcodes didn't match. Try again." = "Passcodes didn't match. Try again."; +//"Passcodes didn't match. Try again." = "Passcodes didn't match. Try again."; "Delete" = "Delete"; "Cancel" = "Cancel"; "_certificate_details_" = "Certificate Server details"; @@ -1160,7 +1205,7 @@ "_waiting_for_" = "Waiting for:"; "_waiting_" = "Waiting …"; "_reachable_wifi_" = "network reachable via Wi-Fi or cable"; -"_ITMS-90076_" = "Due to a change in the Nextcloud application identifier, the settings and password for accessing your cloud are reset, so please re-enter your account data and check your Settings. We are sorry about that."; +"_ITMS-90076_" = "Due to a change in the MagentaCLOUD application identifier, the settings and password for accessing your cloud are reset, so please re-enter your account data and check your Settings. We are sorry about that."; "_password_not_present_" = "Please re-insert your credentials."; "_copy_passphrase_" = "Copy passphrase"; "_ok_copy_passphrase_" = "OK and copy passphrase"; @@ -1318,7 +1363,7 @@ "_delete_in_progress_" = "Delete in progress …"; "_download_in_progress_" = "Download in progress …"; "_upload_in_progress_" = "Upload in progress …"; -"_transfer_in_progress_" = "Transfer in progress …"; +"_transfer_in_progress_" = "Transfer in progress …"; "_in_waiting_" = "In waiting"; "_in_progress_" = "In progress"; "_in_error_" = "In error"; @@ -1533,3 +1578,46 @@ You can stop it at any time, adjust the settings, and enable it again."; "_e2ee_setup_store_privatekey_" = "Private key generation returned no usable key"; "_e2ee_setup_verify_publickey_" = "Public key does not match the certificate"; "_e2ee_setup_generate_passphrase_" = "Unable to generate a passphrase"; + +// MARK: Albums +"_albums_loading_popup_desc_" = "Wait a moment..."; +"_albums_list_nav_title_" = "Albums"; +"_albums_list_new_album_btn_" = "New"; +"_albums_list_loading_msg_" = "Loading..."; +"_albums_list_error_msg_" = "Unable to load albums. Please try again later!"; +"_albums_list_album_name_validation_nonempty_" = "Album name cannot be empty."; +"_albums_list_album_name_validation_min_length_" = "Album name must be at least 3 characters."; +"_albums_list_album_name_validation_max_length_" = "Album name cannot be more than 30 characters."; +"_albums_list_album_name_validation_specials_" = "Album name cannot contain slashes."; +"_albums_list_empty_heading_" = "Create\nAlbums\nfor your\nPhotos"; +"_albums_list_empty_subheading_" = "You can organize all your photos in as many albums as you like. You haven't created an album yet."; +"_albums_list_empty_new_album_btn_" = "Create album"; +"_albums_list_own_albums_heading_" = "My albums"; +"_albums_list_entities_" = "Items"; +"_albums_list_new_album_popup_title_" = "Create new Album"; +"_albums_list_new_album_popup_desc_" = "Please enter an album name between 3 and 30 characters."; +"_albums_list_new_album_popup_hint_" = "Album's name"; +"_albums_list_new_album_popup_positive_btn_" = "Create"; +"_albums_list_new_album_popup_negative_btn_" = "Cancel"; +"_albums_list_rename_album_popup_title_" = "Rename Album"; +"_albums_list_rename_album_popup_desc_" = "Please enter new album name between 3 and 30 characters."; +"_albums_list_rename_album_popup_hint_" = "Album's new name"; +"_albums_list_rename_album_popup_positive_btn_" = "Rename"; +"_albums_list_rename_album_popup_negative_btn_" = "Cancel"; +"_albums_photo_selection_sheet_title_" = "Select items"; +"_albums_photo_selection_sheet_back_btn_" = "Back"; +"_albums_photo_selection_sheet_done_btn_" = "Done"; +"_albums_photos_loading_msg_" = "Loading photos..."; +"_albums_photos_error_msg_" = "Unable to load photos. Please try again later!"; +"_albums_photos_empty_heading_" = "All that's\nmissing are\nyour photos"; +"_albums_photos_empty_subheading_" = "You can add as many photos as you like. A photo can also belong to more than one album."; +"_albums_photos_empty_add_photos_btn_" = "Add photos"; +"_albums_photos_add_photos_btn_" = "Add"; +"_albums_photos_rename_album_btn_" = "Rename Album"; +"_albums_photos_delete_album_btn_" = "Delete Album"; +"_albums_delete_album_popup_title_" = "Delete Album?"; +"_albums_delete_album_popup_desc_" = "Are you sure you want to delete this album? This action cannot be undone."; +"_albums_delete_album_popup_positive_btn_" = "Delete"; +"_albums_delete_album_popup_negative_btn_" = "Cancel"; +"_add_to_album" = "Add to Album"; +"_remove_from_album_" = "Remove from Album"; From 5a64b41da50d04db396cfaa13eb02cab4e0ac7e6 Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:58:19 +0530 Subject: [PATCH 22/40] NMC 1984 - Privacy policy customisation added --- .../NCCollectionViewCommon.swift | 21 +++++++++++++++++++ .../PrivacySettingsViewController.swift | 9 -------- .../SaveSettingsCustomButtonCell.swift | 1 - 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index f8d9b7cefe..a483ad4d4b 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -229,6 +229,14 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let dropInteraction = UIDropInteraction(delegate: self) self.navigationController?.navigationItem.leftBarButtonItems?.first?.customView?.addInteraction(dropInteraction) + + if(!UserDefaults.standard.bool(forKey: "isInitialPrivacySettingsShowed") || isApplicationUpdated()){ + redirectToPrivacyViewController() + + //set current app version + let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String + UserDefaults.standard.set(appVersion, forKey: "CurrentAppVersion") + } if(!UserDefaults.standard.bool(forKey: "isInitialPrivacySettingsShowed") || isApplicationUpdated()){ redirectToPrivacyViewController() @@ -341,6 +349,19 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, removeImageCache(metadatas: self.dataSource.getMetadatas()) } + + func isApplicationUpdated() -> Bool{ + let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" + let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") + return currentVersion != appVersion + } + + func redirectToPrivacyViewController(){ + let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) + let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController + newViewController.modalPresentationStyle = .fullScreen + self.present(newViewController, animated: true, completion: nil) + } func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { let viewController = presentationController.presentedViewController diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index 757d5532d5..2ac54d9635 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -35,9 +35,7 @@ class PrivacySettingsViewController: XLFormViewController{ tableView.reloadData() initializeForm() } - - //MARK: XLForm func initializeForm() { @@ -58,7 +56,6 @@ class PrivacySettingsViewController: XLFormViewController{ section.footerTitle = NSLocalizedString("_privacy_settings_help_text_", comment: "") form.addFormSection(section) - //custom cell section = XLFormSectionDescriptor.formSection(withTitle: "") section.footerTitle = NSLocalizedString("_required_data_collection_help_text_", comment: "") @@ -67,7 +64,6 @@ class PrivacySettingsViewController: XLFormViewController{ XLFormViewController.cellClassesForRowDescriptorTypes()["RequiredDataCollectionCustomCellType"] = RequiredDataCollectionSwitch.self - row = XLFormRowDescriptor(tag: "ButtonDestinationFolder", rowType: "RequiredDataCollectionCustomCellType", title: "") row.cellConfig["requiredDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue @@ -82,7 +78,6 @@ class PrivacySettingsViewController: XLFormViewController{ XLFormViewController.cellClassesForRowDescriptorTypes()["AnalysisDataCollectionCustomCellType"] = AnalysisDataCollectionSwitch.self - row = XLFormRowDescriptor(tag: "AnalysisDataCollectionSwitch", rowType: "AnalysisDataCollectionCustomCellType", title: "") row.cellConfig["analysisDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue @@ -97,13 +92,11 @@ class PrivacySettingsViewController: XLFormViewController{ section.addFormRow(row) - XLFormViewController.cellClassesForRowDescriptorTypes()["SaveSettingsButton"] = SaveSettingsCustomButtonCell.self section = XLFormSectionDescriptor.formSection(withTitle: "") form.addFormSection(section) - row = XLFormRowDescriptor(tag: "SaveSettingsButton", rowType: "SaveSettingsButton", title: "") row.cellConfig["backgroundColor"] = UIColor.clear @@ -111,11 +104,9 @@ class PrivacySettingsViewController: XLFormViewController{ section.addFormRow(row) } - self.form = form } - override func formRowDescriptorValueHasChanged(_ formRow: XLFormRowDescriptor!, oldValue: Any!, newValue: Any!) { super.formRowDescriptorValueHasChanged(formRow, oldValue: oldValue, newValue: newValue) diff --git a/iOSClient/Settings/SaveSettingsCustomButtonCell.swift b/iOSClient/Settings/SaveSettingsCustomButtonCell.swift index 1332b1f92a..b314f62bab 100644 --- a/iOSClient/Settings/SaveSettingsCustomButtonCell.swift +++ b/iOSClient/Settings/SaveSettingsCustomButtonCell.swift @@ -9,7 +9,6 @@ import UIKit import XLForm - class SaveSettingsCustomButtonCell: XLFormButtonCell { @IBOutlet weak var saveSettingsButton: UIButton! From ed49e0b26c9db8b08edb1e3837e28975419f1dc4 Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Tue, 26 Dec 2023 13:29:37 +0530 Subject: [PATCH 23/40] NMC 1984 - Privacy setting view controller added --- iOSClient/Settings/PrivacySettingsViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index 2ac54d9635..60242e5ea8 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -35,7 +35,7 @@ class PrivacySettingsViewController: XLFormViewController{ tableView.reloadData() initializeForm() } - + //MARK: XLForm func initializeForm() { From a7c906547981780d22794b857c1dcc4c31b58e54 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Wed, 9 Apr 2025 14:10:39 +0530 Subject: [PATCH 24/40] NMC 1984 - Privacy policy customisation changes added --- ...mon+CollectionViewDelegateFlowLayout.swift | 43 ++ .../NCCollectionViewCommon.swift | 727 ++++++++++++++++-- 2 files changed, 717 insertions(+), 53 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift index 06795639bc..42bbbd6038 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift @@ -13,4 +13,47 @@ extension NCCollectionViewCommon: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { return sizeForFooterInSection(section: section) } + + func getHeaderHeight() -> CGFloat { + + var size: CGFloat = 0 + // transfer in progress + if headerMenuTransferView, + let metadata = NCManageDatabase.shared.getMetadataFromOcId(NCNetworking.shared.transferInForegorund?.ocId), + metadata.isTransferInForeground { + if !isSearchingMode { + size += NCGlobal.shared.heightHeaderTransfer + } + } else { + NCNetworking.shared.transferInForegorund = nil + } + + if headerMenuButtonsView { + size += NCGlobal.shared.heightButtonsView + } + + return size + } + + func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { + + var headerRichWorkspace: CGFloat = 0 + + if let richWorkspaceText = richWorkspaceText, showDescription { + let trimmed = richWorkspaceText.trimmingCharacters(in: .whitespaces) + if !trimmed.isEmpty && !isSearchingMode { + headerRichWorkspace = UIScreen.main.bounds.size.height / 6 + } + } + + if isSearchingMode || layoutForView?.layout == NCGlobal.shared.layoutGrid || dataSource.numberOfSections() > 1 { + if section == 0 { + return (getHeaderHeight(), headerRichWorkspace, NCGlobal.shared.heightSection) + } else { + return (0, 0, NCGlobal.shared.heightSection) + } + } else { + return (getHeaderHeight(), headerRichWorkspace, 0) + } + } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index a483ad4d4b..252a426bbe 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -10,6 +10,7 @@ import EasyTipView import LucidBanner class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { +//class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionHeaderMenuDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate, NCEmptyDataSetDelegate { @IBOutlet weak var collectionView: UICollectionView! @@ -64,9 +65,19 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, var emptyImageColors: [UIColor]? var emptyTitle: String = "" - var emptyDescription: String = "" - var emptyDataPortaitOffset: CGFloat = 0 - var emptyDataLandscapeOffset: CGFloat = -20 + let maxImageGrid: CGFloat = 7 + var headerMenu: NCSectionHeaderMenu? + var headerMenuTransferView = false + var headerMenuButtonsView: Bool = true + var headerRichWorkspaceDisable: Bool = false + + var groupByField = "name" + + var emptyImage: UIImage? + + internal var emptyDescription: String = "" + internal var emptyDataPortaitOffset: CGFloat = 0 + internal var emptyDataLandscapeOffset: CGFloat = -20 var lastScale: CGFloat = 1.0 var currentScale: CGFloat = 1.0 @@ -80,6 +91,11 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, var numberOfColumns: Int = 0 var lastNumberOfColumns: Int = 0 + var isTransitioning: Bool = false + var selectableDataSource: [RealmSwiftObject] { dataSource.getMetadataSourceForAllSections() } + var pushed: Bool = false + var emptyDataSet: NCEmptyDataSet? + let heightHeaderRecommendations: CGFloat = 160 let heightHeaderSection: CGFloat = 30 @@ -168,7 +184,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, view.backgroundColor = .systemBackground collectionView.backgroundColor = .systemBackground refreshControl.tintColor = .clear - + definesPresentationContext = true + listLayout = NCListLayout() + gridLayout = NCGridLayout() + if enableSearchBar { searchController = UISearchController(searchResultsController: nil) searchController?.searchResultsUpdater = self @@ -177,7 +196,9 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, searchController?.searchBar.delegate = self searchController?.searchBar.autocapitalizationType = .none navigationItem.searchController = searchController - navigationItem.hidesSearchBarWhenScrolling = true + navigationItem.hidesSearchBarWhenScrolling = false + navigationItem.preferredSearchBarPlacement = .inline + navigationItem.backBarButtonItem = UIBarButtonItem(title: NSLocalizedString("_back_", comment: ""), style: .plain, target: nil, action: nil) } // Cell @@ -187,12 +208,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, collectionView.register(UINib(nibName: "NCTransferCell", bundle: nil), forCellWithReuseIdentifier: "transferCell") // Header - collectionView.register(UINib(nibName: "NCSectionFirstHeader", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "sectionFirstHeader") - collectionView.register(UINib(nibName: "NCSectionFirstHeader", bundle: nil), forSupplementaryViewOfKind: mediaSectionHeader, withReuseIdentifier: "sectionFirstHeader") + collectionView.register(UINib(nibName: "NCSectionHeaderMenu", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "sectionHeaderMenu") collectionView.register(UINib(nibName: "NCSectionHeader", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "sectionHeader") - collectionView.register(UINib(nibName: "NCSectionHeader", bundle: nil), forSupplementaryViewOfKind: mediaSectionHeader, withReuseIdentifier: "sectionHeader") - collectionView.register(UINib(nibName: "NCSectionFirstHeaderEmptyData", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "sectionFirstHeaderEmptyData") - collectionView.register(UINib(nibName: "NCSectionFirstHeaderEmptyData", bundle: nil), forSupplementaryViewOfKind: mediaSectionHeader, withReuseIdentifier: "sectionFirstHeaderEmptyData") // Footer collectionView.register(UINib(nibName: "NCSectionFooter", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "sectionFooter") @@ -212,6 +229,9 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.mainNavigationController?.resetPlusButtonAlpha() } } + + // Empty + emptyDataSet = NCEmptyDataSet(view: collectionView, offset: getHeaderHeight(), delegate: self) let longPressedGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPressCollecationView(_:))) longPressedGesture.minimumPressDuration = 0.5 @@ -283,6 +303,11 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, navigationController?.navigationBar.topItem?.title = titlePreviusFolder } navigationItem.title = titleCurrentFolder + navigationController?.setNavigationBarAppearance() + navigationController?.navigationBar.prefersLargeTitles = true + navigationController?.setNavigationBarHidden(false, animated: true) + + appDelegate.activeViewController = self if tabBarSelect == nil { tabBarSelect = NCCollectionViewCommonSelectTabBar(controller: self.controller, viewController: self, delegate: self) @@ -293,11 +318,15 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, Task { await NCNetworking.shared.transferDispatcher.addDelegate(self) - await (self.navigationController as? NCMainNavigationController)?.setNavigationLeftItems() +// await (self.navigationController as? NCMainNavigationController)?.setNavigationLeftItems() await (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() } + /// Magentacloud branding changes hide user account button on left navigation bar +// setNavigationLeftItems() +// setNavigationRightItems() layoutForView = database.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) + gridLayout.column = CGFloat(layoutForView?.columnGrid ?? 3) if isLayoutList { collectionView?.collectionViewLayout = listLayout self.layoutType = global.layoutList @@ -323,6 +352,30 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive(_:)), name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(closeRichWorkspaceWebView), name: NSNotification.Name(rawValue: global.notificationCenterCloseRichWorkspaceWebView), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(changeStatusFolderE2EE(_:)), name: NSNotification.Name(rawValue: global.notificationCenterChangeStatusFolderE2EE), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(changeLayout(_:)), name: NSNotification.Name(rawValue: global.notificationCenterChangeLayout), object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(deleteFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterDeleteFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(copyMoveFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterCopyMoveFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(renameFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterRenameFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(createFolder(_:)), name: NSNotification.Name(rawValue: global.notificationCenterCreateFolder), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(favoriteFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterFavoriteFile), object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(downloadStartFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterDownloadStartFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(downloadedFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterDownloadedFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(downloadCancelFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterDownloadCancelFile), object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(uploadStartFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterUploadStartFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(uploadedFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterUploadedFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(uploadedLivePhoto(_:)), name: NSNotification.Name(rawValue: global.notificationCenterUploadedLivePhoto), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(uploadCancelFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterUploadCancelFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(updateShare(_:)), name: NSNotification.Name(rawValue: global.notificationCenterUpdateShare), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(triggerProgressTask(_:)), name: NSNotification.Name(rawValue: global.notificationCenterProgressTask), object: nil) + + // FIXME: iPAD PDF landscape mode iOS 16 + DispatchQueue.main.async { + self.collectionView?.collectionViewLayout.invalidateLayout() + } } override func viewWillDisappear(_ animated: Bool) { @@ -330,7 +383,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.networking.cancelUnifiedSearchFiles() dismissTip() - + pushed = false + toggleSelect(isOn: false) // Cancel Queue & Retrieves Properties self.networking.downloadThumbnailQueue.cancelAll() self.networking.unifiedSearchQueue.cancelAll() @@ -346,17 +400,38 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCloseRichWorkspaceWebView), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterChangeStatusFolderE2EE), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterChangeLayout), object: nil) + + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDeleteFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCopyMoveFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCopyFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterMoveFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterRenameFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCreateFolder), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterFavoriteFile), object: nil) + + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDownloadStartFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDownloadedFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDownloadCancelFile), object: nil) + + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterUploadStartFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterUploadedFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterUploadedLivePhoto), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterUploadCancelFile), object: nil) + + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterUpdateShare), object: nil) removeImageCache(metadatas: self.dataSource.getMetadatas()) } - func isApplicationUpdated() -> Bool{ + func isApplicationUpdated() -> Bool { let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") return currentVersion != appVersion } - func redirectToPrivacyViewController(){ + func redirectToPrivacyViewController() { let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController newViewController.modalPresentationStyle = .fullScreen @@ -473,8 +548,373 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.present(alertController, animated: true) } + self.collectionView.collectionViewLayout.invalidateLayout() + +// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() + } + + internal func setLayout(layoutForView: NCDBLayoutForView, withSubFolders: Bool = false) async { + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + layoutForView.layout = layoutForView.layout + self.layoutType = layoutForView.layout + + collectionView.reloadData() + + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break + } + + collectionView.collectionViewLayout.invalidateLayout() + } + + @objc func reloadDataSource(_ notification: NSNotification) { + if let userInfo = notification.userInfo as? NSDictionary { + if let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } + } + + if let clearDataSource = userInfo["clearDataSource"] as? Bool, clearDataSource { + self.dataSource.removeAll() + } + } + + reloadDataSource() + } + + @objc func getServerData(_ notification: NSNotification) { + if let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } + } + + getServerData() + } + + @objc func reloadHeader(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + account == session.account + else { return } + + self.collectionView.reloadData() + } + + @objc func changeStatusFolderE2EE(_ notification: NSNotification) { + reloadDataSource() + } + + @objc func closeRichWorkspaceWebView() { + reloadDataSource() + } + + @objc func deleteFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError else { return } + + if error == .success { + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + } else { + NCContentPresenter().showError(error: error) + } + + reloadDataSource() + } + + @objc func copyMoveFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + account == session.account else { return } + + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + + if serverUrl == self.serverUrl { + reloadDataSource() + } + } + + @objc func renameFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let error = userInfo["error"] as? NKError, + account == session.account + else { return } + + if error == .success { + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + } + + if serverUrl == self.serverUrl { + if error != .success { + NCContentPresenter().showError(error: error) + } + reloadDataSource() + } else { + collectionView.reloadData() + } + } + + @objc func createFolder(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let account = userInfo["account"] as? String, + account == session.account, + let withPush = userInfo["withPush"] as? Bool, + let metadata = database.getMetadataFromOcId(ocId) + else { return } + + if isSearchingMode { + return networkSearch() + } + + if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { + reloadDataSource() + } else if withPush, metadata.serverUrl == self.serverUrl { + reloadDataSource() + if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { + if sceneIdentifier == controller?.sceneIdentifier { + pushMetadata(metadata) + } + } else { + pushMetadata(metadata) + } + } + } + + @objc func favoriteFile(_ notification: NSNotification) { + if isSearchingMode { + return networkSearch() + } + + if self is NCFavorite { + return reloadDataSource() + } + + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + serverUrl == self.serverUrl + else { return } + + reloadDataSource() + } + + @objc func downloadStartFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func downloadedFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func downloadCancelFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadStartFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + !isSearchingMode, + let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) + else { return } + + // Header view trasfer + if metadata.isTransferInForeground { + NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) + DispatchQueue.main.async { self.collectionView?.reloadData() } + } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadedFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadedLivePhoto(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadCancelFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func updateShare(_ notification: NSNotification) { + if isSearchingMode { + networkSearch() + } else { + self.dataSource.removeAll() + getServerData() + } } + @objc func triggerProgressTask(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let progressNumber = userInfo["progress"] as? NSNumber, + let totalBytes = userInfo["totalBytes"] as? Int64, + let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, + let ocId = userInfo["ocId"] as? String, + let ocIdTransfer = userInfo["ocIdTransfer"] as? String, + let session = userInfo["session"] as? String + else { return } + + let chunk: Int = userInfo["chunk"] as? Int ?? 0 + let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false + + let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) + + // HEADER +// if self.headerMenuTransferView, transfer.session.contains("upload") { +// self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) +// self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) +// } + + DispatchQueue.main.async { + if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { + if NCNetworking.shared.transferInForegorund?.ocId == ocId { + NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue + } else { + NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) + self.collectionView.reloadData() + } + self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue + } else { + guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, + let cell = self.collectionView?.cellForItem(at: indexPath), + let cell = cell as? NCCellProtocol else { return } + if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { + cell.fileProgressView?.isHidden = true + cell.fileProgressView?.progress = .zero + cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) + if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { + cell.writeInfoDateSize(date: metadata.date, size: metadata.size) + } else { + cell.fileInfoLabel?.text = "" + cell.fileSubinfoLabel?.text = "" + } + } else { + cell.fileProgressView?.isHidden = false + cell.fileProgressView?.progress = progressNumber.floatValue + cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) + let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal + if status == NCGlobal.shared.metadataStatusDownloading { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) + } else if status == NCGlobal.shared.metadataStatusUploading { + if totalBytes > 0 { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) + } else { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" + } + } + } + } + } + } + + // MARK: - Layout + + func setNavigationLeftItems() { + navigationItem.title = titleCurrentFolder + } + func getNavigationTitle() -> String { let tblAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) if let tblAccount, @@ -517,6 +957,36 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.navigationItem.title = self.titleCurrentFolder } + // MARK: - Empty + + func emptyDataSetView(_ view: NCEmptyView) { + + self.emptyDataSet?.setOffset(getHeaderHeight()) + if isSearchingMode { + view.emptyImage.image = UIImage(named: "search")?.image(color: .gray, size: UIScreen.main.bounds.width) + if self.dataSourceTask?.state == .running { + view.emptyTitle.text = NSLocalizedString("_search_in_progress_", comment: "") + } else { + view.emptyTitle.text = NSLocalizedString("_search_no_record_found_", comment: "") + } + view.emptyDescription.text = NSLocalizedString("_search_instruction_", comment: "") + } else if self.dataSourceTask?.state == .running { + view.emptyImage.image = UIImage(named: "networkInProgress")?.image(color: .gray, size: UIScreen.main.bounds.width) + view.emptyTitle.text = NSLocalizedString("_request_in_progress_", comment: "") + view.emptyDescription.text = "" + } else { + if serverUrl.isEmpty { + view.emptyImage.image = emptyImage + view.emptyTitle.text = NSLocalizedString(emptyTitle, comment: "") + view.emptyDescription.text = NSLocalizedString(emptyDescription, comment: "") + } else { + view.emptyImage.image = UIImage(named: "folder_nmcloud") + view.emptyTitle.text = NSLocalizedString("_files_no_files_", comment: "") + view.emptyDescription.text = NSLocalizedString("_no_file_pull_down_", comment: "") + } + } + } + // MARK: - SEARCH func searchController(enabled: Bool) { @@ -547,8 +1017,73 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { - if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { - networkSearch() + if isSearchingMode, + searchResultText?.count ?? 0 >= 2 { + Task { + await self.search() + } +// if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { +// networkSearch() + } + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + self.networking.cancelUnifiedSearchFiles() + + self.isSearchingMode = false + self.literalSearch = "" + self.providers?.removeAll() + self.dataSource.removeAll() + Task { + await self.reloadDataSource() + } + // + mainNavigationController?.hiddenPlusButton(false) + } + + // MARK: - TAP EVENT + + func tapMoreListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) + } + + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) + } + + func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) { + if isEditMode { return } + guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } + + NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) +// NCDownloadAction.shared.openShare(viewController: self, metadata: metadata, page: .sharing) + TealiumHelper.shared.trackEvent(title: "magentacloud-app.filebrowser.sharing", data: ["": ""]) + appDelegate.adjust.trackEvent(TriggerEvent(Sharing.rawValue)) + } + + func tapMoreGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { + if isEditMode { return } + guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } +// toggleMenu(metadata: metadata, image: image) + if namedButtonMore == NCGlobal.shared.buttonMoreMore || namedButtonMore == NCGlobal.shared.buttonMoreLock { + toggleMenu(metadata: metadata, image: image) + } else if namedButtonMore == NCGlobal.shared.buttonMoreStop { + Task { + await cancelSession(metadata: metadata) + } + } + } + + func tapRichWorkspace(_ sender: Any) { + if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { + if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { + viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" + viewerRichWorkspace.serverUrl = serverUrl + viewerRichWorkspace.delegate = self + + navigationController.modalPresentationStyle = .fullScreen + self.present(navigationController, animated: true, completion: nil) + } } } @@ -625,6 +1160,54 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, func longPressMoreGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func tapRecommendations(with metadata: tableMetadata) { + didSelectMetadata(metadata, withOcIds: false) + } + + func tapButtonSwitch(_ sender: Any) { + guard !isTransitioning else { return } + isTransitioning = true + + guard let layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) else { return } + + if layoutForView.layout == NCGlobal.shared.layoutGrid { + layoutForView.layout = NCGlobal.shared.layoutList + } else { + layoutForView.layout = NCGlobal.shared.layoutGrid + } + self.layoutForView = NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) + self.collectionView.reloadData() + self.collectionView.collectionViewLayout.invalidateLayout() + self.collectionView.setCollectionViewLayout(layoutForView.layout == NCGlobal.shared.layoutList ? self.listLayout : self.gridLayout, animated: true) {_ in self.isTransitioning = false } + } + + func tapButtonOrder(_ sender: Any) { + +// if let titleButtonHeader = NCKeychain().getTitleButtonHeader(account: session.account), !titleButtonHeader.isEmpty { +// layoutForView?.titleButtonHeader = titleButtonHeader +// } +// NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView!) + + let sortMenu = NCSortMenu() + sortMenu.toggleMenu(viewController: self, account: session.account, key: layoutKey, sortButton: sender as? UIButton, serverUrl: serverUrl) + } + + func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func longPressListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func tapShareListItem(with objectId: String, indexPath: IndexPath, sender: Any) { } + + func longPressMoreListItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func longPressGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func longPressMoreGridItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func tapButtonTransfer(_ sender: Any) { } + + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } + @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) } @@ -749,11 +1332,27 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // MARK: - DataSource - @MainActor - func reloadDataSource() async { - if !isSearchingMode { - Task.detached { - if await self.isRecommendationActived() { + @objc func reloadDataSource() async { + + // get auto upload folder + autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() + autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: session.urlBase, userId: session.userId, account: session.account) + + // get layout for view + layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) + // set GroupField for Grid + if !isSearchingMode && layoutForView?.layout == NCGlobal.shared.layoutGrid { + groupByField = "classFile" + } else { + groupByField = "name" + } + + if isSearchingMode { + isDirectoryEncrypted = false + } else { + isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) + if isRecommendationActived { + Task.detached { await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) } } @@ -763,7 +1362,12 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, delegate.transferReloadData(serverUrl: self.serverUrl) } - await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + await mainNavigationController?.updateMenuOption() + (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + self.refreshControl.endRefreshing() + self.collectionView.reloadData() + self.setNavigationRightItems() + } } func getServerData(forced: Bool = false) async { } @@ -903,39 +1507,56 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } } - // MARK: - Header size - - func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, - heightHeaderRecommendations: CGFloat, - heightHeaderSection: CGFloat) { - var heightHeaderRichWorkspace: CGFloat = 0 - var heightHeaderRecommendations: CGFloat = 0 - var heightHeaderSection: CGFloat = 0 - - if showDescription, - !isSearchingMode, - let richWorkspaceText = self.richWorkspaceText, - !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { - heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 - } + func pushViewController(viewController: UIViewController) { + if pushed { return } - if isRecommendationActived, - !isSearchingMode, - NCPreferences().showRecommendedFiles, - !self.database.getRecommendedFiles(account: self.session.account).isEmpty { - heightHeaderRecommendations = self.heightHeaderRecommendations - heightHeaderSection = self.heightHeaderSection - } + pushed = true + navigationController?.pushViewController(viewController, animated: true) + } + + // MARK: - Header size - if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { - if section == 0 { - return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) - } else { - return (0, 0, self.heightHeaderSection) - } - } else { - return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) +// func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, +// heightHeaderRecommendations: CGFloat, +// heightHeaderSection: CGFloat) { +// var heightHeaderRichWorkspace: CGFloat = 0 +// var heightHeaderRecommendations: CGFloat = 0 +// var heightHeaderSection: CGFloat = 0 +// +// if showDescription, +// !isSearchingMode, +// let richWorkspaceText = self.richWorkspaceText, +// !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { +// heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 +// } +// +// if isRecommendationActived, +// !isSearchingMode, +// NCKeychain().showRecommendedFiles, +// !self.database.getRecommendedFiles(account: self.session.account).isEmpty { +// heightHeaderRecommendations = self.heightHeaderRecommendations +// heightHeaderSection = self.heightHeaderSection +// } +// +// if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { +// if section == 0 { +// return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) +// } else { +// return (0, 0, self.heightHeaderSection) +// } +// } else { +// return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) +// } +// } + + func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { + if headerMenuTransferView, + NCNetworking.shared.isOnline, + let results = database.getResultsMetadatas(predicate: NSPredicate(format: "status IN %@", [global.metadataStatusWaitUpload, global.metadataStatusUploading])), + !results.isEmpty { + return Array(results) } + return nil } func sizeForHeaderInSection(section: Int) -> CGSize { @@ -944,7 +1565,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let isIphone = UIDevice.current.userInterfaceIdiom == .phone if self.dataSource.isEmpty() { - height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset) + height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset, isHeaderMenuTransferViewEnabled: isHeaderMenuTransferViewEnabled() != nil) } else if isEditMode || (isLandscape && isIphone) { return CGSize.zero } else { From e0e0991bb0ddd7b0c6b61accc702e0b29ecf206e Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Tue, 30 Sep 2025 22:00:01 +0530 Subject: [PATCH 25/40] NMC 1984 - Privacy policy customisation added --- .../NCCollectionViewCommon.swift | 532 +++++++++++++----- 1 file changed, 403 insertions(+), 129 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 252a426bbe..bbe18b767f 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -4,13 +4,14 @@ import UIKit import SwiftUI +import Realm import RealmSwift import NextcloudKit import EasyTipView import LucidBanner +import MoEngageInApps -class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { -//class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionHeaderMenuDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate, NCEmptyDataSetDelegate { +class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionHeaderMenuDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate, NCEmptyDataSetDelegate { @IBOutlet weak var collectionView: UICollectionView! @@ -32,6 +33,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, var serverUrl: String = "" var isEditMode = false var isDirectoryE2EE = false + var isDirectoryEncrypted = false var fileSelect: [String] = [] var metadataFolder: tableMetadata? var richWorkspaceText: String? @@ -41,6 +43,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, var networkSearchInProgress: Bool = false var layoutForView: NCDBLayoutForView? var searchDataSourceTask: URLSessionTask? + var dataSourceTask: URLSessionTask? var providers: [NKSearchProvider]? var searchResults: [NKSearchResult]? var listLayout = NCListLayout() @@ -51,9 +54,17 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, var tabBarSelect: NCCollectionViewCommonSelectTabBar? var attributesZoomIn: UIMenuElement.Attributes = [] var attributesZoomOut: UIMenuElement.Attributes = [] - var tipViewAccounts: EasyTipView? var syncMetadatasTask: Task? + let maxImageGrid: CGFloat = 7 +// var headerMenu: NCSectionFirstHeader? + var tipViewAccounts: EasyTipView? + var tipViewAutoUpload: EasyTipView? + var headerMenu: NCSectionHeaderMenu? + var headerMenuTransferView = false + var headerMenuButtonsView: Bool = true + var headerRichWorkspaceDisable: Bool = false + // DECLARE var layoutKey = "" var titleCurrentFolder = "" @@ -104,7 +115,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, NCSession.shared.getSession(controller: tabBarController) } - var isLayoutPhoto: Bool { + internal var isLayoutPhoto: Bool { layoutForView?.layout == global.layoutPhotoRatio || layoutForView?.layout == global.layoutPhotoSquare } @@ -142,7 +153,22 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, (self.tabBarController as? NCMainTabBarController)?.sceneIdentifier ?? "" } - var isNumberOfItemsInAllSectionsNull: Bool { + var defaultPredicate: NSPredicate { + let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, NCGlobal.shared.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) + return predicate + } + + var personalFilesOnlyPredicate: NSPredicate { + let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND (ownerId == %@ || ownerId == '') AND mountType == '' AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, session.userId, global.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) + return predicate + } + + @MainActor + internal var windowScene: UIWindowScene? { + SceneManager.shared.getWindowScene(controller: self.tabBarController as? NCMainTabBarController) + } + + internal var isNumberOfItemsInAllSectionsNull: Bool { var totalItems = 0 for section in 0.. Bool { @@ -440,7 +491,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { let viewController = presentationController.presentedViewController - + if viewController is NCViewerRichWorkspaceWebView { closeRichWorkspaceWebView() } @@ -483,23 +534,147 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.present(newViewController!, animated: true, completion: nil) } + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + + if let frame = tabBarController?.tabBar.frame { + tabBarSelect?.hostingController?.view.frame = frame + } + } + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + + func transferChange(status: String, + account: String, + fileName: String, + serverUrl: String, + selector: String?, + ocId: String, + destination: String?, + error: NKError) { + Task { + if error != .success, + error.errorCode != global.errorResourceNotFound { + await showErrorBanner(controller: self.controller, + errorDescription: error.errorDescription, + errorCode: error.errorCode) + } + guard session.account == account else { + return + } + + await self.debouncer.call { + switch status { + // UPLOADED, UPLOADED LIVEPHOTO, DELETE + case self.global.networkingStatusUploaded, + self.global.networkingStatusDelete, + self.global.networkingStatusCopyMove: + if self.isSearchingMode { + self.networkSearch() + } else if self.serverUrl == serverUrl || destination == self.serverUrl { + await self.reloadDataSource() + } + // DOWNLOAD + case self.global.networkingStatusDownloaded: + if serverUrl == self.serverUrl || self.serverUrl.isEmpty { + await self.reloadDataSource() + } + case self.global.networkingStatusDownloadCancel: + if serverUrl == self.serverUrl { + await self.reloadDataSource() + } + // CREATE FOLDER + case self.global.networkingStatusCreateFolder: + if serverUrl == self.serverUrl, + selector != self.global.selectorUploadAutoUpload, + let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { + self.pushMetadata(metadata) + } + // RENAME + case self.global.networkingStatusRename: + if self.isSearchingMode { + self.networkSearch() + } else if self.serverUrl == serverUrl { + await self.reloadDataSource() + } + // FAVORITE + case self.global.networkingStatusFavorite: + if self.isSearchingMode { + self.networkSearch() + } else if self is NCFavorite { + await self.reloadDataSource() + } else if self.serverUrl == serverUrl { + await self.reloadDataSource() + } + default: + break + } + } + } + } + + func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) { + Task { + await self.debouncer.call { + if requestData { + if self.isSearchingMode { + self.networkSearch() + } else if ( self.serverUrl == serverUrl) || serverUrl == nil { + Task { + await self.getServerData() + } + } + } else { + if self.isSearchingMode { + guard status != self.global.metadataStatusWaitDelete, + status != self.global.metadataStatusWaitRename, + status != self.global.metadataStatusWaitMove, + status != self.global.metadataStatusWaitCopy, + status != self.global.metadataStatusWaitFavorite else { + return + } + self.networkSearch() + } else if ( self.serverUrl == serverUrl) || serverUrl == nil { + Task { + await self.reloadDataSource() + } + } + } + } + } + } + // MARK: - NotificationCenter @objc func applicationWillResignActive(_ notification: NSNotification) { - mainNavigationController?.resetPlusButtonAlpha() + self.mainNavigationController?.menuPlus?.resetPlusButtonAlpha() +// self.resetPlusButtonAlpha() +// self.refreshControl.endRefreshing() } - @objc func closeRichWorkspaceWebView() { - Task { - await self.reloadDataSource() + @objc func reloadAvatar(_ notification: NSNotification) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.showTipAccounts() } + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError, + error.errorCode != global.errorNotModified else { return } + /// Magentacloud branding changes hide user account button on left navigation bar + setNavigationLeftItems() } - // MARK: - Layout + @objc func changeTheming(_ notification: NSNotification) { + self.reloadDataSource() + } - func changeLayout(layoutForView: NCDBLayoutForView) { - let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) - let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 + @objc func changeLayout(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let layoutForView = userInfo["layoutForView"] as? NCDBLayoutForView, + account == session.account, + serverUrl == self.serverUrl + else { return } func changeLayout(withSubFolders: Bool) { if self.layoutForView?.layout == layoutForView.layout { @@ -545,9 +720,29 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in changeLayout(withSubFolders: false) })) +// if self.layoutForView?.layout == layoutForView.layout { +// self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) +// self.reloadDataSource() +// return +// } +// +// self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) +// layoutForView.layout = layoutForView.layout +// self.layoutType = layoutForView.layout +// +// collectionView.reloadData() - self.present(alertController, animated: true) + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break } + self.collectionView.collectionViewLayout.invalidateLayout() // (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() @@ -833,16 +1028,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, collectionView?.reloadData() } } - - @objc func updateShare(_ notification: NSNotification) { - if isSearchingMode { - networkSearch() - } else { - self.dataSource.removeAll() - getServerData() - } - } - + @objc func triggerProgressTask(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, let progressNumber = userInfo["progress"] as? NSNumber, @@ -857,14 +1043,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) - - // HEADER -// if self.headerMenuTransferView, transfer.session.contains("upload") { -// self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) -// self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) -// } - DispatchQueue.main.async { +// DispatchQueue.main.async { if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { if NCNetworking.shared.transferInForegorund?.ocId == ocId { NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue @@ -874,7 +1054,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, + guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId), let cell = self.collectionView?.cellForItem(at: indexPath), let cell = cell as? NCCellProtocol else { return } if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { @@ -906,8 +1086,22 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } } } +// } + } + + @objc func updateShare(_ notification: NSNotification) { + if isSearchingMode { + networkSearch() + } else { +// self.dataSource.removeAll() + getServerData() } } + + @objc func updateIcons() { + collectionView.reloadData() +// reloadDataSource() + } // MARK: - Layout @@ -916,14 +1110,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } func getNavigationTitle() -> String { - let tblAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) - if let tblAccount, - !tblAccount.alias.isEmpty { - return tblAccount.alias + let tableAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) + if let tableAccount, + !tableAccount.alias.isEmpty { + return tableAccount.alias } return NCBrandOptions.shared.brand } + func accountSettingsDidDismiss(tableAccount: tableAccount?, controller: NCMainTabBarController?) { } + @MainActor func startGUIGetServerData() { self.dataSource.setGetServerData(false) @@ -956,6 +1152,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.navigationItem.titleView = nil self.navigationItem.title = self.titleCurrentFolder } + + func resetPlusButtonAlpha(animated: Bool = true) { } + + func isHiddenPlusButton(_ isHidden: Bool) { } // MARK: - Empty @@ -1012,8 +1212,21 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } // TIP dismissTip() - // - mainNavigationController?.hiddenPlusButton(true) + + // (+) + self.mainNavigationController?.menuPlus?.hiddenPlusButton(true) + + if !isSearchingMode { + self.isSearchingMode = true + self.dataSource.removeAll() + self.collectionView.reloadData() + } +// isSearchingMode = true +// self.providers?.removeAll() +// self.dataSource.removeAll() +// self.reloadDataSource() +// // TIP +// dismissTip() } func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { @@ -1028,17 +1241,12 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - self.networking.cancelUnifiedSearchFiles() - + NCNetworking.shared.cancelUnifiedSearchFiles() self.isSearchingMode = false self.literalSearch = "" self.providers?.removeAll() self.dataSource.removeAll() - Task { - await self.reloadDataSource() - } - // - mainNavigationController?.hiddenPlusButton(false) + self.reloadDataSource() } // MARK: - TAP EVENT @@ -1046,6 +1254,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, func tapMoreListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) } + + func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) + } func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) @@ -1073,6 +1285,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } } } + + func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } +// toggleMenu(metadata: metadata, image: image) + Task { + await cancelSession(metadata: metadata) + } + } + + func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } func tapRichWorkspace(_ sender: Any) { if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { @@ -1088,15 +1310,47 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - self.networking.cancelUnifiedSearchFiles() - + // (+) + self.mainNavigationController?.menuPlus?.hiddenPlusButton(false) + self.isSearchingMode = false self.networkSearchInProgress = false - self.literalSearch = "" - self.providers?.removeAll() - self.dataSource.removeAll() + self.searchResultText = nil + self.searchResultStore = nil + Task { - await self.reloadDataSource() + await searchOperationHandle.cancel() + await reloadDataSource() + + // Restore Layout + if let layoutForViewLayoutStore { + let layoutForView = database.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) + layoutForView.layout = layoutForViewLayoutStore + await setLayout(layoutForView: layoutForView) + } + layoutForViewLayoutStore = nil + + // update Option menu + await mainNavigationController?.updateMenuOption() + } + } + + func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?) { + toggleMenu(metadata: metadata, image: image) + } + + @MainActor + func setSearchBarLoading(_ loading: Bool) { + guard let textField = searchController?.searchBar.searchTextField else { + return + } + if loading { + let spinner = UIActivityIndicatorView(style: .medium) + spinner.startAnimating() + textField.rightView = spinner + textField.rightViewMode = .always + } else { + textField.rightView = nil } // mainNavigationController?.hiddenPlusButton(false) @@ -1182,32 +1436,28 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } func tapButtonOrder(_ sender: Any) { - -// if let titleButtonHeader = NCKeychain().getTitleButtonHeader(account: session.account), !titleButtonHeader.isEmpty { -// layoutForView?.titleButtonHeader = titleButtonHeader -// } -// NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView!) - let sortMenu = NCSortMenu() sortMenu.toggleMenu(viewController: self, account: session.account, key: layoutKey, sortButton: sender as? UIButton, serverUrl: serverUrl) } - func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func tapButtonTransfer(_ sender: Any) { } - func longPressListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } - func tapShareListItem(with objectId: String, indexPath: IndexPath, sender: Any) { } + func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func longPressListItem(with objectId: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) { + } + func longPressListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } func longPressMoreListItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } func longPressGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } func longPressMoreGridItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func tapButtonTransfer(_ sender: Any) { } - - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } - @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) } @@ -1229,7 +1479,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, becomeFirstResponder() if !serverUrl.isEmpty { - listMenuItems.append(UIMenuItem(title: NSLocalizedString("_paste_file_", comment: ""), action: #selector(pasteFilesMenu(_:)))) + listMenuItems.append(UIMenuItem(title: NSLocalizedString("_paste_file_", comment: ""), action: #selector(pasteFilesMenu))) } if !listMenuItems.isEmpty { @@ -1238,16 +1488,23 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } } + // MARK: - Transfer Delegate + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + + func tranferChange(status: String, metadata: tableMetadata, error: NKError) { } + // MARK: - Menu Item override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - if #selector(pasteFilesMenu(_:)) == action { + + if #selector(pasteFilesMenu) == action { if !UIPasteboard.general.items.isEmpty, !(metadataFolder?.e2eEncrypted ?? false) { return true } - } else if #selector(copyMenuFile(_:)) == action { + } else if #selector(copyMenuFile) == action { return true - } else if #selector(moveMenuFile(_:)) == action { + } else if #selector(moveMenuFile) == action { return true } @@ -1332,12 +1589,12 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // MARK: - DataSource - @objc func reloadDataSource() async { + @objc func reloadDataSource() { // get auto upload folder autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: session.urlBase, userId: session.userId, account: session.account) - + // get layout for view layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) // set GroupField for Grid @@ -1346,16 +1603,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } else { groupByField = "name" } - if isSearchingMode { isDirectoryEncrypted = false } else { isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) - if isRecommendationActived { - Task.detached { - await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) - } - } } await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in @@ -1370,7 +1621,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } } - func getServerData(forced: Bool = false) async { } + func getServerData() { + } @objc func networkSearch() { guard !networkSearchInProgress else { @@ -1506,38 +1758,55 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } } } - + func pushViewController(viewController: UIViewController) { if pushed { return } pushed = true navigationController?.pushViewController(viewController, animated: true) } - + // MARK: - Header size -// func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, -// heightHeaderRecommendations: CGFloat, -// heightHeaderSection: CGFloat) { -// var heightHeaderRichWorkspace: CGFloat = 0 -// var heightHeaderRecommendations: CGFloat = 0 -// var heightHeaderSection: CGFloat = 0 +// func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { +// var headerRichWorkspace: CGFloat = 0 // -// if showDescription, -// !isSearchingMode, -// let richWorkspaceText = self.richWorkspaceText, -// !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { -// heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 -// } +// func getHeaderHeight() -> CGFloat { +// var size: CGFloat = 0 // -// if isRecommendationActived, -// !isSearchingMode, -// NCKeychain().showRecommendedFiles, -// !self.database.getRecommendedFiles(account: self.session.account).isEmpty { -// heightHeaderRecommendations = self.heightHeaderRecommendations -// heightHeaderSection = self.heightHeaderSection +// if isHeaderMenuTransferViewEnabled() != nil { +// if !isSearchingMode { +// size += global.heightHeaderTransfer +// } +// } +// if headerMenuButtonsView { +// size += NCGlobal.shared.heightButtonsView +// } +// return size // } // +//// func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, +//// heightHeaderRecommendations: CGFloat, +//// heightHeaderSection: CGFloat) { +//// var heightHeaderRichWorkspace: CGFloat = 0 +//// var heightHeaderRecommendations: CGFloat = 0 +//// var heightHeaderSection: CGFloat = 0 +//// +//// if showDescription, +//// !isSearchingMode, +//// let richWorkspaceText = self.richWorkspaceText, +//// !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { +//// heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 +//// } +//// +//// if isRecommendationActived, +//// !isSearchingMode, +//// NCKeychain().showRecommendedFiles, +//// !self.database.getRecommendedFiles(account: self.session.account).isEmpty { +//// heightHeaderRecommendations = self.heightHeaderRecommendations +//// heightHeaderSection = self.heightHeaderSection +//// } +// // if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { // if section == 0 { // return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) @@ -1546,6 +1815,15 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // } // } else { // return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) +//// +//// if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { +//// if section == 0 { +//// return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) +//// } else { +//// return (0, 0, self.heightHeaderSection) +//// } +//// } else { +//// return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) // } // } @@ -1569,8 +1847,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } else if isEditMode || (isLandscape && isIphone) { return CGSize.zero } else { - let (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) = getHeaderHeight(section: section) - height = heightHeaderRichWorkspace + heightHeaderRecommendations + heightHeaderSection + let (heightHeaderCommands, heightHeaderRichWorkspace, heightHeaderSection) = getHeaderHeight(section: section) + height = heightHeaderCommands + heightHeaderRichWorkspace + heightHeaderSection } return CGSize(width: collectionView.frame.width, height: height) @@ -1579,26 +1857,22 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // MARK: - Footer size func sizeForFooterInSection(section: Int) -> CGSize { - guard let controller else { - return CGSize.zero - } let sections = dataSource.numberOfSections() - let bottomAreaInsets: CGFloat = controller.tabBar.safeAreaInsets.bottom == 0 ? 34 : 0 - let height = controller.tabBar.frame.height + bottomAreaInsets - - if isEditMode { - return CGSize(width: collectionView.frame.width, height: 90 + height) - } - - if isSearchingMode { - return CGSize(width: collectionView.frame.width, height: 50) - } + let metadataForSection = self.dataSource.getMetadataForSection(section) + let isPaginated = metadataForSection?.lastSearchResult?.isPaginated ?? false + let metadatasCount: Int = metadataForSection?.lastSearchResult?.entries.count ?? 0 + var size = CGSize(width: collectionView.frame.width, height: 0) if section == sections - 1 { - return CGSize(width: collectionView.frame.width, height: height) + size.height += 85 } else { - return CGSize(width: collectionView.frame.width, height: 0) + size.height += 1 + } + + if isSearchingMode && isPaginated && metadatasCount > 0 { + size.height += 30 } + return size } func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } From 3796f3114bc03e14e95eca7f5924627a5bd96da3 Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:58:19 +0530 Subject: [PATCH 26/40] NMC 1984 - Privacy policy customisation added --- .../Collection Common/NCCollectionViewCommon.swift | 13 +++++++++++++ .../Settings/PrivacySettingsViewController.swift | 4 +--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index bbe18b767f..cd325a6591 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -488,6 +488,19 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS newViewController.modalPresentationStyle = .fullScreen self.present(newViewController, animated: true, completion: nil) } + + func isApplicationUpdated() -> Bool{ + let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" + let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") + return currentVersion != appVersion + } + + func redirectToPrivacyViewController(){ + let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) + let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController + newViewController.modalPresentationStyle = .fullScreen + self.present(newViewController, animated: true, completion: nil) + } func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { let viewController = presentationController.presentedViewController diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index 60242e5ea8..c7ba269c0e 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -13,7 +13,7 @@ import XLForm class PrivacySettingsViewController: XLFormViewController{ - @objc public var isShowSettingsButton: Bool = false + @objc public var isShowSettingsButton: Bool = false override func viewDidLoad() { super.viewDidLoad() @@ -37,9 +37,7 @@ class PrivacySettingsViewController: XLFormViewController{ } //MARK: XLForm - func initializeForm() { - let form : XLFormDescriptor = XLFormDescriptor() as XLFormDescriptor form.rowNavigationOptions = XLFormRowNavigationOptions.stopDisableRow From f290ff086807c1ebf969b8b9ad324aaf30abe74f Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Tue, 26 Dec 2023 13:29:37 +0530 Subject: [PATCH 27/40] NMC 1984 - Privacy setting view controller added --- iOSClient/Settings/PrivacySettingsViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index c7ba269c0e..e4497c92bc 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -37,7 +37,9 @@ class PrivacySettingsViewController: XLFormViewController{ } //MARK: XLForm + func initializeForm() { + let form : XLFormDescriptor = XLFormDescriptor() as XLFormDescriptor form.rowNavigationOptions = XLFormRowNavigationOptions.stopDisableRow From 24da72f41ecb7a354c4401c98eb89b59e26b6b85 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Wed, 9 Apr 2025 14:10:39 +0530 Subject: [PATCH 28/40] NMC 1984 - Privacy policy customisation changes added --- .../NCCollectionViewCommon.swift | 468 +++++++++++++++--- 1 file changed, 406 insertions(+), 62 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index cd325a6591..acdbeefa65 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -489,13 +489,13 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.present(newViewController, animated: true, completion: nil) } - func isApplicationUpdated() -> Bool{ + func isApplicationUpdated() -> Bool { let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") return currentVersion != appVersion } - func redirectToPrivacyViewController(){ + func redirectToPrivacyViewController() { let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController newViewController.modalPresentationStyle = .fullScreen @@ -1116,6 +1116,399 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // reloadDataSource() } + // MARK: - Layout + + func changeLayout(layoutForView: NCDBLayoutForView) { + let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) + let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 + + func changeLayout(withSubFolders: Bool) { + if self.layoutForView?.layout == layoutForView.layout { + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + Task { + await self.reloadDataSource() + } + return + } + + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + layoutForView.layout = layoutForView.layout + self.layoutType = layoutForView.layout + + collectionView.reloadData() + + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break + } + + self.collectionView.collectionViewLayout.invalidateLayout() + + Task { + await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + } + } + + if serverUrl == homeServer || numFoldersLayoutsForView == 1 { + changeLayout(withSubFolders: false) + } else { + let alertController = UIAlertController(title: NSLocalizedString("_propagate_layout_", comment: ""), message: nil, preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default, handler: { _ in + changeLayout(withSubFolders: true) + })) + alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in + changeLayout(withSubFolders: false) + })) + + self.present(alertController, animated: true) + } + self.collectionView.collectionViewLayout.invalidateLayout() + +// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() + } + + @objc func reloadDataSource(_ notification: NSNotification) { + if let userInfo = notification.userInfo as? NSDictionary { + if let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } + } + + if let clearDataSource = userInfo["clearDataSource"] as? Bool, clearDataSource { + self.dataSource.removeAll() + } + } + + reloadDataSource() + } + + @objc func getServerData(_ notification: NSNotification) { + if let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } + } + + getServerData() + } + + @objc func reloadHeader(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + account == session.account + else { return } + + self.collectionView.reloadData() + } + + @objc func changeStatusFolderE2EE(_ notification: NSNotification) { + reloadDataSource() + } + + @objc func closeRichWorkspaceWebView() { + reloadDataSource() + } + + @objc func deleteFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError else { return } + + if error == .success { + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + } else { + NCContentPresenter().showError(error: error) + } + + reloadDataSource() + } + + @objc func copyMoveFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + account == session.account else { return } + + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + + if serverUrl == self.serverUrl { + reloadDataSource() + } + } + + @objc func renameFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let error = userInfo["error"] as? NKError, + account == session.account + else { return } + + if error == .success { + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + } + + if serverUrl == self.serverUrl { + if error != .success { + NCContentPresenter().showError(error: error) + } + reloadDataSource() + } else { + collectionView.reloadData() + } + } + + @objc func createFolder(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let account = userInfo["account"] as? String, + account == session.account, + let withPush = userInfo["withPush"] as? Bool, + let metadata = database.getMetadataFromOcId(ocId) + else { return } + + if isSearchingMode { + return networkSearch() + } + + if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { + reloadDataSource() + } else if withPush, metadata.serverUrl == self.serverUrl { + reloadDataSource() + if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { + if sceneIdentifier == controller?.sceneIdentifier { + pushMetadata(metadata) + } + } else { + pushMetadata(metadata) + } + } + } + + @objc func favoriteFile(_ notification: NSNotification) { + if isSearchingMode { + return networkSearch() + } + + if self is NCFavorite { + return reloadDataSource() + } + + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + serverUrl == self.serverUrl + else { return } + + reloadDataSource() + } + + @objc func downloadStartFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func downloadedFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func downloadCancelFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadStartFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + !isSearchingMode, + let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) + else { return } + + // Header view trasfer + if metadata.isTransferInForeground { + NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) + DispatchQueue.main.async { self.collectionView?.reloadData() } + } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadedFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadedLivePhoto(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadCancelFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func updateShare(_ notification: NSNotification) { + if isSearchingMode { + networkSearch() + } else { + self.dataSource.removeAll() + getServerData() + } + } + + @objc func triggerProgressTask(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let progressNumber = userInfo["progress"] as? NSNumber, + let totalBytes = userInfo["totalBytes"] as? Int64, + let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, + let ocId = userInfo["ocId"] as? String, + let ocIdTransfer = userInfo["ocIdTransfer"] as? String, + let session = userInfo["session"] as? String + else { return } + + let chunk: Int = userInfo["chunk"] as? Int ?? 0 + let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false + + let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) + + // HEADER +// if self.headerMenuTransferView, transfer.session.contains("upload") { +// self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) +// self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) +// } + + DispatchQueue.main.async { + if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { + if NCNetworking.shared.transferInForegorund?.ocId == ocId { + NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue + } else { + NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) + self.collectionView.reloadData() + } + self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue + } else { + guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, + let cell = self.collectionView?.cellForItem(at: indexPath), + let cell = cell as? NCCellProtocol else { return } + if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { + cell.fileProgressView?.isHidden = true + cell.fileProgressView?.progress = .zero + cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) + if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { + cell.writeInfoDateSize(date: metadata.date, size: metadata.size) + } else { + cell.fileInfoLabel?.text = "" + cell.fileSubinfoLabel?.text = "" + } + } else { + cell.fileProgressView?.isHidden = false + cell.fileProgressView?.progress = progressNumber.floatValue + cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) + let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal + if status == NCGlobal.shared.metadataStatusDownloading { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) + } else if status == NCGlobal.shared.metadataStatusUploading { + if totalBytes > 0 { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) + } else { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" + } + } + } + } + } + } + // MARK: - Layout func setNavigationLeftItems() { @@ -1471,6 +1864,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS func longPressMoreGridItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func tapButtonTransfer(_ sender: Any) { } + + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } + @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) } @@ -1607,7 +2004,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // get auto upload folder autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: session.urlBase, userId: session.userId, account: session.account) - // get layout for view layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) // set GroupField for Grid @@ -1779,66 +2175,14 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS navigationController?.pushViewController(viewController, animated: true) } - // MARK: - Header size + func pushViewController(viewController: UIViewController) { + if pushed { return } -// func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { -// var headerRichWorkspace: CGFloat = 0 -// -// func getHeaderHeight() -> CGFloat { -// var size: CGFloat = 0 -// -// if isHeaderMenuTransferViewEnabled() != nil { -// if !isSearchingMode { -// size += global.heightHeaderTransfer -// } -// } -// if headerMenuButtonsView { -// size += NCGlobal.shared.heightButtonsView -// } -// return size -// } -// -//// func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, -//// heightHeaderRecommendations: CGFloat, -//// heightHeaderSection: CGFloat) { -//// var heightHeaderRichWorkspace: CGFloat = 0 -//// var heightHeaderRecommendations: CGFloat = 0 -//// var heightHeaderSection: CGFloat = 0 -//// -//// if showDescription, -//// !isSearchingMode, -//// let richWorkspaceText = self.richWorkspaceText, -//// !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { -//// heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 -//// } -//// -//// if isRecommendationActived, -//// !isSearchingMode, -//// NCKeychain().showRecommendedFiles, -//// !self.database.getRecommendedFiles(account: self.session.account).isEmpty { -//// heightHeaderRecommendations = self.heightHeaderRecommendations -//// heightHeaderSection = self.heightHeaderSection -//// } -// -// if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { -// if section == 0 { -// return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) -// } else { -// return (0, 0, self.heightHeaderSection) -// } -// } else { -// return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) -//// -//// if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { -//// if section == 0 { -//// return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) -//// } else { -//// return (0, 0, self.heightHeaderSection) -//// } -//// } else { -//// return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) -// } -// } + pushed = true + navigationController?.pushViewController(viewController, animated: true) + } + + // MARK: - Header size func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { if headerMenuTransferView, From 1391b561ecc647837d62c85a9a90037f30f20081 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Tue, 30 Sep 2025 22:00:01 +0530 Subject: [PATCH 29/40] NMC 1984 - Privacy policy customisation added --- .../NCCollectionViewCommon.swift | 123 +++++++----------- 1 file changed, 46 insertions(+), 77 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index acdbeefa65..e9a0f53a61 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -482,19 +482,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return currentVersion != appVersion } - func redirectToPrivacyViewController() { - let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) - let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController - newViewController.modalPresentationStyle = .fullScreen - self.present(newViewController, animated: true, completion: nil) - } - - func isApplicationUpdated() -> Bool { - let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" - let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") - return currentVersion != appVersion - } - func redirectToPrivacyViewController() { let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController @@ -865,7 +852,17 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS if serverUrl == self.serverUrl { reloadDataSource() + } + + @objc func reloadAvatar(_ notification: NSNotification) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.showTipAccounts() } + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError, + error.errorCode != global.errorNotModified else { return } + /// Magentacloud branding changes hide user account button on left navigation bar + setNavigationLeftItems() } @objc func renameFile(_ notification: NSNotification) { @@ -1122,53 +1119,30 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 - func changeLayout(withSubFolders: Bool) { - if self.layoutForView?.layout == layoutForView.layout { - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) - Task { - await self.reloadDataSource() - } - return - } - - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) - layoutForView.layout = layoutForView.layout - self.layoutType = layoutForView.layout - - collectionView.reloadData() - - switch layoutForView.layout { - case global.layoutList: - self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) - case global.layoutGrid: - self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) - case global.layoutPhotoSquare, global.layoutPhotoRatio: - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) - default: - break - } - - self.collectionView.collectionViewLayout.invalidateLayout() - Task { - await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() - } + if self.layoutForView?.layout == layoutForView.layout { + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) + self.reloadDataSource() + return } - if serverUrl == homeServer || numFoldersLayoutsForView == 1 { - changeLayout(withSubFolders: false) - } else { - let alertController = UIAlertController(title: NSLocalizedString("_propagate_layout_", comment: ""), message: nil, preferredStyle: .alert) + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) + layoutForView.layout = layoutForView.layout + self.layoutType = layoutForView.layout - alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default, handler: { _ in - changeLayout(withSubFolders: true) - })) - alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in - changeLayout(withSubFolders: false) - })) + collectionView.reloadData() - self.present(alertController, animated: true) + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break } + self.collectionView.collectionViewLayout.invalidateLayout() // (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() @@ -1433,16 +1407,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS collectionView?.reloadData() } } - - @objc func updateShare(_ notification: NSNotification) { - if isSearchingMode { - networkSearch() - } else { - self.dataSource.removeAll() - getServerData() - } - } - + @objc func triggerProgressTask(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, let progressNumber = userInfo["progress"] as? NSNumber, @@ -1457,14 +1422,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) - - // HEADER -// if self.headerMenuTransferView, transfer.session.contains("upload") { -// self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) -// self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) -// } - DispatchQueue.main.async { +// DispatchQueue.main.async { if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { if NCNetworking.shared.transferInForegorund?.ocId == ocId { NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue @@ -1474,7 +1433,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, + guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId), let cell = self.collectionView?.cellForItem(at: indexPath), let cell = cell as? NCCellProtocol else { return } if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { @@ -1506,8 +1465,22 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } } +// } + } + + @objc func updateShare(_ notification: NSNotification) { + if isSearchingMode { + networkSearch() + } else { +// self.dataSource.removeAll() + getServerData() } } + + @objc func updateIcons() { + collectionView.reloadData() +// reloadDataSource() + } // MARK: - Layout @@ -1864,10 +1837,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS func longPressMoreGridItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func tapButtonTransfer(_ sender: Any) { } - - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } - @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) } @@ -2181,7 +2150,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS pushed = true navigationController?.pushViewController(viewController, animated: true) } - + // MARK: - Header size func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { From 3d7536291c909406bf956a81cdfadf1508996ee0 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Wed, 17 Dec 2025 18:29:46 +0530 Subject: [PATCH 30/40] NMC 1984 - Privacy policy customisation added --- .../Data/NCManageDatabase+LocalFile.swift | 40 +- .../Data/NCManageDatabase+Metadata.swift | 66 + ...mon+CollectionViewDelegateFlowLayout.swift | 43 - .../NCCollectionViewCommon+SyncMetadata.swift | 14 +- .../NCCollectionViewCommon.swift | 1383 +++++------------ .../NCBackgroundLocationUploadManager.swift | 19 +- iOSClient/NCGlobal.swift | 10 +- iOSClient/Networking/NCConfigServer.swift | 25 +- .../Networking/NCNetworking+Download.swift | 62 +- .../Networking/NCNetworking+LivePhoto.swift | 25 +- iOSClient/Networking/NCNetworking+Task.swift | 25 +- .../Networking/NCNetworking+Upload.swift | 21 +- .../Networking/NCNetworking+WebDAV.swift | 69 +- iOSClient/Networking/NCNetworking.swift | 198 +++ .../Networking/NCNetworkingProcess.swift | 1 - iOSClient/Networking/NCService.swift | 25 +- .../en.lproj/Localizable.strings | 6 - 17 files changed, 919 insertions(+), 1113 deletions(-) diff --git a/iOSClient/Data/NCManageDatabase+LocalFile.swift b/iOSClient/Data/NCManageDatabase+LocalFile.swift index 2146dec1ad..334e5fba25 100644 --- a/iOSClient/Data/NCManageDatabase+LocalFile.swift +++ b/iOSClient/Data/NCManageDatabase+LocalFile.swift @@ -29,14 +29,17 @@ extension NCManageDatabase { // MARK: - Realm Write - /// Adds or updates multiple local file entries corresponding to the given metadata array. - /// Uses async Realm read + single write transaction. Assumes `tableLocalFile` has a primary key. /// - Parameters: - /// - metadatas: Array of `tableMetadata` to map into `tableLocalFile`. - /// - offline: Optional override for the `offline` flag applied to all items. - func addLocalFilesAsync(metadatas: [tableMetadata], offline: Bool? = nil) async { - guard !metadatas.isEmpty else { - return + /// - metadata: The `tableMetadata` containing file details. + /// - offline: Optional flag to mark the file as available offline. + /// - Returns: Nothing. Realm write is performed asynchronously. + func addLocalFileAsync(metadata: tableMetadata, offline: Bool? = nil) async { + // Read (non-blocking): safely detach from Realm thread + let existing: tableLocalFile? = performRealmRead { realm in + realm.objects(tableLocalFile.self) + .filter(NSPredicate(format: "ocId == %@", metadata.ocId)) + .first + .map { tableLocalFile(value: $0) } } // Extract ocIds for efficient lookup @@ -59,20 +62,19 @@ extension NCManageDatabase { // Reuse existing object or create a new one let local = existingMap[metadata.ocId] ?? tableLocalFile() - local.account = metadata.account - local.etag = metadata.etag - local.exifDate = NSDate() - local.exifLatitude = "-1" - local.exifLongitude = "-1" - local.ocId = metadata.ocId - local.fileName = metadata.fileName + addObject.account = metadata.account + addObject.etag = metadata.etag + addObject.exifDate = NSDate() + addObject.exifLatitude = "-1" + addObject.exifLongitude = "-1" + addObject.ocId = metadata.ocId + addObject.fileName = metadata.fileName - if let offline { - local.offline = offline - } - - realm.add(local, update: .all) + if let offline { + addObject.offline = offline } + + realm.add(addObject, update: .all) } } diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index 67ed7ad2f2..5c3b840691 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -151,10 +151,37 @@ extension tableMetadata { (fileNameView as NSString).deletingPathExtension } + var isRenameable: Bool { + if !NCMetadataPermissions.canRename(self) { + return false + } + if lock { + return false + } + if !isDirectoryE2EE && e2eEncrypted { + return false + } + return true + } + + var isPrintable: Bool { + if isDocumentViewableOnly { + return false + } + if ["application/pdf", "com.adobe.pdf"].contains(contentType) || contentType.hasPrefix("text/") || classFile == NKTypeClassFile.image.rawValue { + return true + } + return false + } + var isSavebleInCameraRoll: Bool { return (classFile == NKTypeClassFile.image.rawValue && contentType != "image/svg+xml") || classFile == NKTypeClassFile.video.rawValue } + var isDocumentViewableOnly: Bool { + sharePermissionsCollaborationServices == NCPermissions().permissionReadShare && classFile == NKTypeClassFile.document.rawValue + } + var isAudioOrVideo: Bool { return classFile == NKTypeClassFile.audio.rawValue || classFile == NKTypeClassFile.video.rawValue } @@ -344,6 +371,17 @@ extension tableMetadata { return !lock || (lockOwner == user && lockOwnerType == 0) } + // Return if is sharable + func isSharable() -> Bool { + guard let capabilities = NCNetworking.shared.capabilities[account] else { + return false + } + if !capabilities.fileSharingApiEnabled || (capabilities.e2EEEnabled && isDirectoryE2EE), !e2eEncrypted { + return false + } + return !e2eEncrypted + } + /// Returns a detached (unmanaged) deep copy of the current `tableMetadata` object. /// /// - Note: Primitive properties and lists of primitive values (for example `shareType`) @@ -931,6 +969,34 @@ extension NCManageDatabase { .map { $0.detachedCopy() } } ?? [] } + + func getMediaMetadatas(predicate: NSPredicate, sorted: String? = nil, ascending: Bool = false) -> ThreadSafeArray? { + + do { + let realm = try Realm() + if let sorted { + var results: [tableMetadata] = [] + switch sorted {//NCPreferences().mediaSortDate { + case "date": + results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.date as Date) > ($1.date as Date) } + case "creationDate": + results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.creationDate as Date) > ($1.creationDate as Date) } + case "uploadDate": + results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.uploadDate as Date) > ($1.uploadDate as Date) } + default: + let results = realm.objects(tableMetadata.self).filter(predicate) + return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) + } + return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) + } else { + let results = realm.objects(tableMetadata.self).filter(predicate) + return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) + } + } catch let error as NSError { +// NextcloudKit.shared.nkCommonInstance.writeLog("Could not access database: \(error)") + } + return nil + } func getMetadatas(predicate: NSPredicate, sortedByKeyPath: String, diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift index 42bbbd6038..06795639bc 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift @@ -13,47 +13,4 @@ extension NCCollectionViewCommon: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { return sizeForFooterInSection(section: section) } - - func getHeaderHeight() -> CGFloat { - - var size: CGFloat = 0 - // transfer in progress - if headerMenuTransferView, - let metadata = NCManageDatabase.shared.getMetadataFromOcId(NCNetworking.shared.transferInForegorund?.ocId), - metadata.isTransferInForeground { - if !isSearchingMode { - size += NCGlobal.shared.heightHeaderTransfer - } - } else { - NCNetworking.shared.transferInForegorund = nil - } - - if headerMenuButtonsView { - size += NCGlobal.shared.heightButtonsView - } - - return size - } - - func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { - - var headerRichWorkspace: CGFloat = 0 - - if let richWorkspaceText = richWorkspaceText, showDescription { - let trimmed = richWorkspaceText.trimmingCharacters(in: .whitespaces) - if !trimmed.isEmpty && !isSearchingMode { - headerRichWorkspace = UIScreen.main.bounds.size.height / 6 - } - } - - if isSearchingMode || layoutForView?.layout == NCGlobal.shared.layoutGrid || dataSource.numberOfSections() > 1 { - if section == 0 { - return (getHeaderHeight(), headerRichWorkspace, NCGlobal.shared.heightSection) - } else { - return (0, 0, NCGlobal.shared.heightSection) - } - } else { - return (getHeaderHeight(), headerRichWorkspace, 0) - } - } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift index 5e0254858d..571306d2de 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift @@ -21,7 +21,7 @@ extension NCCollectionViewCommon { // If a sync task is already running, do not start a new one if let task = syncMetadatasTask, !task.isCancelled { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .info, message: "Exit: Another sync is already running. Skipping this one.", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .info, message: "Exit: Another sync is already running. Skipping this one.", consoleOnly: true) return } @@ -45,9 +45,9 @@ extension NCCollectionViewCommon { func stopSyncMetadata() async { if let task = syncMetadatasTask { if task.isCancelled { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .stop, message: "Sync Metadata for \(self.serverUrl) was already cancelled.", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .stop, message: "Sync Metadata for \(self.serverUrl) was already cancelled.", consoleOnly: true) } else { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .stop, message: "Stopping active Sync Metadata for \(self.serverUrl).", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .stop, message: "Stopping active Sync Metadata for \(self.serverUrl).", consoleOnly: true) } } @@ -74,7 +74,7 @@ extension NCCollectionViewCommon { return } let identifier = self.serverUrl + "_syncMetadata" - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .start, message: "Start Sync Metadata for \(self.serverUrl)") + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .start, message: "Start Sync Metadata for \(self.serverUrl)") // Always cancel and clear all tracked URLSessionTask on any exit path defer { @@ -85,7 +85,7 @@ extension NCCollectionViewCommon { // If a readFile for this serverUrl is already in-flight, do nothing if await networking.networkingTasks.isReading(identifier: identifier) { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .debug, message: "ReadFile for this \(self.serverUrl) is already in-flight.", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .debug, message: "ReadFile for this \(self.serverUrl) is already in-flight.", consoleOnly: true) return } @@ -151,9 +151,9 @@ extension NCCollectionViewCommon { // If this folder failed, skip it but keep processing others if resultsReadFolder.error == .success { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .network, message: "Read correctly: \(serverUrl)", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .network, message: "Read correctly: \(serverUrl)", consoleOnly: true) } else { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .error, message: "Read failed for \(serverUrl) with error: \(resultsReadFolder.error.errorDescription)") + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .error, message: "Read failed for \(serverUrl) with error: \(resultsReadFolder.error.errorDescription)") return } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index e9a0f53a61..c502da76a7 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -11,7 +11,7 @@ import EasyTipView import LucidBanner import MoEngageInApps -class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionHeaderMenuDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate, NCEmptyDataSetDelegate { +class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionFirstHeaderDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { @IBOutlet weak var collectionView: UICollectionView! @@ -59,22 +59,13 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // var headerMenu: NCSectionFirstHeader? var tipViewAccounts: EasyTipView? - var tipViewAutoUpload: EasyTipView? - var headerMenu: NCSectionHeaderMenu? - var headerMenuTransferView = false - var headerMenuButtonsView: Bool = true - var headerRichWorkspaceDisable: Bool = false - + var selectableDataSource: [RealmSwiftObject] { dataSource.getMetadataSourceForAllSections() } + // DECLARE var layoutKey = "" var titleCurrentFolder = "" var titlePreviusFolder: String? var enableSearchBar: Bool = false - var headerRichWorkspaceDisable: Bool = false - - var emptyImageName: String? - var emptyImageColors: [UIColor]? - var emptyTitle: String = "" let maxImageGrid: CGFloat = 7 var headerMenu: NCSectionHeaderMenu? @@ -83,12 +74,15 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var headerRichWorkspaceDisable: Bool = false var groupByField = "name" + var headerRichWorkspaceDisable: Bool = false - var emptyImage: UIImage? + var emptyImageName: String? + var emptyImageColors: [UIColor]? + var emptyTitle: String = "" - internal var emptyDescription: String = "" - internal var emptyDataPortaitOffset: CGFloat = 0 - internal var emptyDataLandscapeOffset: CGFloat = -20 + var emptyDescription: String = "" + var emptyDataPortaitOffset: CGFloat = 0 + var emptyDataLandscapeOffset: CGFloat = -20 var lastScale: CGFloat = 1.0 var currentScale: CGFloat = 1.0 @@ -102,11 +96,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var numberOfColumns: Int = 0 var lastNumberOfColumns: Int = 0 - var isTransitioning: Bool = false - var selectableDataSource: [RealmSwiftObject] { dataSource.getMetadataSourceForAllSections() } - var pushed: Bool = false - var emptyDataSet: NCEmptyDataSet? - let heightHeaderRecommendations: CGFloat = 160 let heightHeaderSection: CGFloat = 30 @@ -115,7 +104,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS NCSession.shared.getSession(controller: tabBarController) } - internal var isLayoutPhoto: Bool { + var isLayoutPhoto: Bool { layoutForView?.layout == global.layoutPhotoRatio || layoutForView?.layout == global.layoutPhotoSquare } @@ -153,22 +142,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS (self.tabBarController as? NCMainTabBarController)?.sceneIdentifier ?? "" } - var defaultPredicate: NSPredicate { - let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, NCGlobal.shared.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) - return predicate - } - - var personalFilesOnlyPredicate: NSPredicate { - let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND (ownerId == %@ || ownerId == '') AND mountType == '' AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, session.userId, global.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) - return predicate - } - - @MainActor - internal var windowScene: UIWindowScene? { - SceneManager.shared.getWindowScene(controller: self.tabBarController as? NCMainTabBarController) - } - - internal var isNumberOfItemsInAllSectionsNull: Bool { + var isNumberOfItemsInAllSectionsNull: Bool { var totalItems = 0 for section in 0.. Bool { - let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" - let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") - return currentVersion != appVersion + + removeImageCache(metadatas: self.dataSource.getMetadatas()) } - func redirectToPrivacyViewController() { - let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) - let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController - newViewController.modalPresentationStyle = .fullScreen - self.present(newViewController, animated: true, completion: nil) + func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { + let viewController = presentationController.presentedViewController + + if viewController is NCViewerRichWorkspaceWebView { + closeRichWorkspaceWebView() + } } func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { @@ -515,10 +399,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } @objc func updateIcons() { -// collectionView.reloadData() - Task { - await self.reloadDataSource() - } + collectionView.reloadData() } func isApplicationUpdated() -> Bool { @@ -534,11 +415,182 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.present(newViewController!, animated: true, completion: nil) } - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() + // MARK: - Transfer Delegate + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + + func transferChange(status: String, metadatasError: [tableMetadata: NKError]) { + switch status { + // DELETE + case self.global.networkingStatusDelete: + let errorForThisServer = metadatasError.first { entry in + let (key, value) = entry + return key.serverUrl == self.serverUrl && value != .success + }?.value + + let needLoadDataSource = metadatasError.contains { entry in + let (key, value) = entry + return key.serverUrl == self.serverUrl && value == .success + } + + if let error = errorForThisServer { + NCContentPresenter().showError(error: error) + } + + if self.isSearchingMode { + self.networkSearch() + } else if needLoadDataSource { + Task { + await self.reloadDataSource() + } + } else { + Task.detached { + if await self.isRecommendationActived() { + await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) + } + } + } + default: + break + } + } + + func transferChange(status: String, metadata: tableMetadata, error: NKError) { + guard session.account == metadata.account else { return } + + if error != .success { + NCContentPresenter().showError(error: error) + } + + DispatchQueue.main.async { + switch status { + // UPLOADED, UPLOADED LIVEPHOTO + case self.global.networkingStatusUploaded, self.global.networkingStatusUploadedLivePhoto: + self.debouncer.call { + if self.isSearchingMode { + self.networkSearch() + } else if self.serverUrl == metadata.serverUrl { + Task { + await self.reloadDataSource() + } + } + } + // DOWNLOAD + case self.global.networkingStatusDownloading: + Task { + if metadata.serverUrl == self.serverUrl { + await self.reloadDataSource() + } + } + case self.global.networkingStatusDownloaded: + Task { + if metadata.serverUrl == self.serverUrl { + await self.reloadDataSource() + } + } + case self.global.networkingStatusDownloadCancel: + Task { + if metadata.serverUrl == self.serverUrl { + await self.reloadDataSource() + } + } + // CREATE FOLDER + case self.global.networkingStatusCreateFolder: + if metadata.serverUrl == self.serverUrl, metadata.sessionSelector != self.global.selectorUploadAutoUpload { + self.pushMetadata(metadata) + } + // RENAME + case self.global.networkingStatusRename: + self.debouncer.call { + if self.isSearchingMode { + self.networkSearch() + } else if self.serverUrl == metadata.serverUrl { + Task { + await self.reloadDataSource() + } + } + } + // FAVORITE + case self.global.networkingStatusFavorite: + self.debouncer.call { + if self.isSearchingMode { + self.networkSearch() + } else if self is NCFavorite { + Task { + await self.reloadDataSource() + } + } else if self.serverUrl == metadata.serverUrl { + Task { + await self.reloadDataSource() + } + } + } + default: + break + } + } + } + + func transferReloadData(serverUrl: String?, status: Int?) { + self.debouncer.call { + if self.isSearchingMode { + guard status != self.global.metadataStatusWaitDelete, + status != self.global.metadataStatusWaitRename, + status != self.global.metadataStatusWaitMove, + status != self.global.metadataStatusWaitCopy, + status != self.global.metadataStatusWaitFavorite else { + return + } + self.networkSearch() + } else if ( self.serverUrl == serverUrl) || serverUrl == nil { + Task { + await self.reloadDataSource() + } + } + } + } + + func transferRequestData(serverUrl: String?) { + self.debouncer.call { + if self.isSearchingMode { + self.networkSearch() + } else if ( self.serverUrl == serverUrl) || serverUrl == nil { + Task { + await self.getServerData() + } + } + } + } + + func transferCopy(metadata: tableMetadata, destination: String, error: NKError) { + if error != .success { + NCContentPresenter().showError(error: error) + } + + if isSearchingMode { + return networkSearch() + } + + if metadata.serverUrl == self.serverUrl || destination == self.serverUrl { + Task { + await self.reloadDataSource() + } + } + } + + func transferMove(metadata: tableMetadata, destination: String, error: NKError) { + if error != .success { + NCContentPresenter().showError(error: error) + } + + if isSearchingMode { + return networkSearch() + } - if let frame = tabBarController?.tabBar.frame { - tabBarSelect?.hostingController?.view.frame = frame + if metadata.serverUrl == self.serverUrl || destination == self.serverUrl { + Task { + await self.reloadDataSource() + } } } @@ -810,698 +862,81 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } @objc func closeRichWorkspaceWebView() { - reloadDataSource() + Task { + await self.reloadDataSource() + } } - @objc func deleteFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let error = userInfo["error"] as? NKError else { return } + // MARK: - Layout - if error == .success { - if isSearchingMode { - return networkSearch() - } + func changeLayout(layoutForView: NCDBLayoutForView) { + let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) + let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) + func changeLayout(withSubFolders: Bool) { + if self.layoutForView?.layout == layoutForView.layout { + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + Task { + await self.reloadDataSource() } + return } - } else { - NCContentPresenter().showError(error: error) - } - - reloadDataSource() - } - @objc func copyMoveFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - account == session.account else { return } + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + layoutForView.layout = layoutForView.layout + self.layoutType = layoutForView.layout - if isSearchingMode { - return networkSearch() - } + collectionView.reloadData() - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break } - } - if serverUrl == self.serverUrl { - reloadDataSource() - } + self.collectionView.collectionViewLayout.invalidateLayout() - @objc func reloadAvatar(_ notification: NSNotification) { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.showTipAccounts() + Task { + await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + } } - guard let userInfo = notification.userInfo as NSDictionary?, - let error = userInfo["error"] as? NKError, - error.errorCode != global.errorNotModified else { return } - /// Magentacloud branding changes hide user account button on left navigation bar - setNavigationLeftItems() - } - @objc func renameFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let account = userInfo["account"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let error = userInfo["error"] as? NKError, - account == session.account - else { return } - - if error == .success { - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - } - - if serverUrl == self.serverUrl { - if error != .success { - NCContentPresenter().showError(error: error) - } - reloadDataSource() - } else { - collectionView.reloadData() - } - } - - @objc func createFolder(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let account = userInfo["account"] as? String, - account == session.account, - let withPush = userInfo["withPush"] as? Bool, - let metadata = database.getMetadataFromOcId(ocId) - else { return } - - if isSearchingMode { - return networkSearch() - } - - if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { - reloadDataSource() - } else if withPush, metadata.serverUrl == self.serverUrl { - reloadDataSource() - if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { - if sceneIdentifier == controller?.sceneIdentifier { - pushMetadata(metadata) - } - } else { - pushMetadata(metadata) - } - } - } - - @objc func favoriteFile(_ notification: NSNotification) { - if isSearchingMode { - return networkSearch() - } - - if self is NCFavorite { - return reloadDataSource() - } - - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - serverUrl == self.serverUrl - else { return } - - reloadDataSource() - } - - @objc func downloadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func downloadedFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func downloadCancelFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - !isSearchingMode, - let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) - else { return } - - // Header view trasfer - if metadata.isTransferInForeground { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) - DispatchQueue.main.async { self.collectionView?.reloadData() } - } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadedFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadedLivePhoto(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadCancelFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func triggerProgressTask(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let progressNumber = userInfo["progress"] as? NSNumber, - let totalBytes = userInfo["totalBytes"] as? Int64, - let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, - let ocId = userInfo["ocId"] as? String, - let ocIdTransfer = userInfo["ocIdTransfer"] as? String, - let session = userInfo["session"] as? String - else { return } - - let chunk: Int = userInfo["chunk"] as? Int ?? 0 - let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false - - let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) - -// DispatchQueue.main.async { - if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { - if NCNetworking.shared.transferInForegorund?.ocId == ocId { - NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue - } else { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) - self.collectionView.reloadData() - } - self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue - } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId), - let cell = self.collectionView?.cellForItem(at: indexPath), - let cell = cell as? NCCellProtocol else { return } - if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { - cell.fileProgressView?.isHidden = true - cell.fileProgressView?.progress = .zero - cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) - if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { - cell.writeInfoDateSize(date: metadata.date, size: metadata.size) - } else { - cell.fileInfoLabel?.text = "" - cell.fileSubinfoLabel?.text = "" - } - } else { - cell.fileProgressView?.isHidden = false - cell.fileProgressView?.progress = progressNumber.floatValue - cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) - let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal - if status == NCGlobal.shared.metadataStatusDownloading { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) - } else if status == NCGlobal.shared.metadataStatusUploading { - if totalBytes > 0 { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) - } else { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" - } - } - } - } -// } - } - - @objc func updateShare(_ notification: NSNotification) { - if isSearchingMode { - networkSearch() - } else { -// self.dataSource.removeAll() - getServerData() - } - } - - @objc func updateIcons() { - collectionView.reloadData() -// reloadDataSource() - } - - // MARK: - Layout - - func changeLayout(layoutForView: NCDBLayoutForView) { - let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) - let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 - - - if self.layoutForView?.layout == layoutForView.layout { - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) - self.reloadDataSource() - return - } - - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) - layoutForView.layout = layoutForView.layout - self.layoutType = layoutForView.layout - - collectionView.reloadData() - - switch layoutForView.layout { - case global.layoutList: - self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) - case global.layoutGrid: - self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) - case global.layoutPhotoSquare, global.layoutPhotoRatio: - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) - default: - break - } - - self.collectionView.collectionViewLayout.invalidateLayout() - -// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() - } - - @objc func reloadDataSource(_ notification: NSNotification) { - if let userInfo = notification.userInfo as? NSDictionary { - if let serverUrl = userInfo["serverUrl"] as? String { - if serverUrl != self.serverUrl { - return - } - } - - if let clearDataSource = userInfo["clearDataSource"] as? Bool, clearDataSource { - self.dataSource.removeAll() - } - } - - reloadDataSource() - } - - @objc func getServerData(_ notification: NSNotification) { - if let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String { - if serverUrl != self.serverUrl { - return - } - } - - getServerData() - } - - @objc func reloadHeader(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let account = userInfo["account"] as? String, - account == session.account - else { return } - - self.collectionView.reloadData() - } - - @objc func changeStatusFolderE2EE(_ notification: NSNotification) { - reloadDataSource() - } - - @objc func closeRichWorkspaceWebView() { - reloadDataSource() - } - - @objc func deleteFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let error = userInfo["error"] as? NKError else { return } - - if error == .success { - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - } else { - NCContentPresenter().showError(error: error) - } - - reloadDataSource() - } - - @objc func copyMoveFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - account == session.account else { return } - - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - - if serverUrl == self.serverUrl { - reloadDataSource() - } - } - - @objc func renameFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let account = userInfo["account"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let error = userInfo["error"] as? NKError, - account == session.account - else { return } - - if error == .success { - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - } - - if serverUrl == self.serverUrl { - if error != .success { - NCContentPresenter().showError(error: error) - } - reloadDataSource() - } else { - collectionView.reloadData() - } - } - - @objc func createFolder(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let account = userInfo["account"] as? String, - account == session.account, - let withPush = userInfo["withPush"] as? Bool, - let metadata = database.getMetadataFromOcId(ocId) - else { return } - - if isSearchingMode { - return networkSearch() - } - - if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { - reloadDataSource() - } else if withPush, metadata.serverUrl == self.serverUrl { - reloadDataSource() - if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { - if sceneIdentifier == controller?.sceneIdentifier { - pushMetadata(metadata) - } - } else { - pushMetadata(metadata) - } - } - } - - @objc func favoriteFile(_ notification: NSNotification) { - if isSearchingMode { - return networkSearch() - } - - if self is NCFavorite { - return reloadDataSource() - } - - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - serverUrl == self.serverUrl - else { return } - - reloadDataSource() - } - - @objc func downloadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func downloadedFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func downloadCancelFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - !isSearchingMode, - let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) - else { return } - - // Header view trasfer - if metadata.isTransferInForeground { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) - DispatchQueue.main.async { self.collectionView?.reloadData() } - } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadedFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadedLivePhoto(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadCancelFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() + if serverUrl == homeServer || numFoldersLayoutsForView == 1 { + changeLayout(withSubFolders: false) } else { - collectionView?.reloadData() - } - } - - @objc func triggerProgressTask(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let progressNumber = userInfo["progress"] as? NSNumber, - let totalBytes = userInfo["totalBytes"] as? Int64, - let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, - let ocId = userInfo["ocId"] as? String, - let ocIdTransfer = userInfo["ocIdTransfer"] as? String, - let session = userInfo["session"] as? String - else { return } - - let chunk: Int = userInfo["chunk"] as? Int ?? 0 - let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false + let alertController = UIAlertController(title: NSLocalizedString("_propagate_layout_", comment: ""), message: nil, preferredStyle: .alert) - let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) - -// DispatchQueue.main.async { - if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { - if NCNetworking.shared.transferInForegorund?.ocId == ocId { - NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue - } else { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) - self.collectionView.reloadData() - } - self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue - } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId), - let cell = self.collectionView?.cellForItem(at: indexPath), - let cell = cell as? NCCellProtocol else { return } - if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { - cell.fileProgressView?.isHidden = true - cell.fileProgressView?.progress = .zero - cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) - if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { - cell.writeInfoDateSize(date: metadata.date, size: metadata.size) - } else { - cell.fileInfoLabel?.text = "" - cell.fileSubinfoLabel?.text = "" - } - } else { - cell.fileProgressView?.isHidden = false - cell.fileProgressView?.progress = progressNumber.floatValue - cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) - let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal - if status == NCGlobal.shared.metadataStatusDownloading { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) - } else if status == NCGlobal.shared.metadataStatusUploading { - if totalBytes > 0 { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) - } else { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" - } - } - } - } -// } - } + alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default, handler: { _ in + changeLayout(withSubFolders: true) + })) + alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in + changeLayout(withSubFolders: false) + })) - @objc func updateShare(_ notification: NSNotification) { - if isSearchingMode { - networkSearch() - } else { -// self.dataSource.removeAll() - getServerData() + self.present(alertController, animated: true) } } - @objc func updateIcons() { - collectionView.reloadData() -// reloadDataSource() - } - - // MARK: - Layout - - func setNavigationLeftItems() { - navigationItem.title = titleCurrentFolder - } - func getNavigationTitle() -> String { - let tableAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) - if let tableAccount, - !tableAccount.alias.isEmpty { - return tableAccount.alias + let tblAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) + if let tblAccount, + !tblAccount.alias.isEmpty { + return tblAccount.alias } return NCBrandOptions.shared.brand } - func accountSettingsDidDismiss(tableAccount: tableAccount?, controller: NCMainTabBarController?) { } + func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } @MainActor func startGUIGetServerData() { self.dataSource.setGetServerData(false) + self.collectionView.reloadData() // Don't show spinner on iPad root folder if UIDevice.current.userInterfaceIdiom == .pad, @@ -1564,8 +999,29 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS view.emptyDescription.text = NSLocalizedString("_no_file_pull_down_", comment: "") } } + + let spinner = UIActivityIndicatorView(style: .medium) + spinner.startAnimating() + + let container = UIView() + container.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(spinner) + + spinner.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + spinner.centerXAnchor.constraint(equalTo: container.centerXAnchor), + spinner.centerYAnchor.constraint(equalTo: container.centerYAnchor) + ]) + + self.navigationItem.titleView = container } - + + @MainActor + func restoreDefaultTitle() { + self.navigationItem.titleView = nil + self.navigationItem.title = self.titleCurrentFolder + } + // MARK: - SEARCH func searchController(enabled: Bool) { @@ -1575,6 +1031,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS searchController?.searchBar.alpha = 1 } else { searchController?.searchBar.alpha = 0.3 + } } @@ -1591,59 +1048,41 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } // TIP dismissTip() - - // (+) - self.mainNavigationController?.menuPlus?.hiddenPlusButton(true) - - if !isSearchingMode { - self.isSearchingMode = true - self.dataSource.removeAll() - self.collectionView.reloadData() - } -// isSearchingMode = true -// self.providers?.removeAll() -// self.dataSource.removeAll() -// self.reloadDataSource() -// // TIP -// dismissTip() + // + mainNavigationController?.hiddenPlusButton(true) } func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { - if isSearchingMode, - searchResultText?.count ?? 0 >= 2 { - Task { - await self.search() - } -// if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { -// networkSearch() + if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { + networkSearch() } } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - NCNetworking.shared.cancelUnifiedSearchFiles() + self.networking.cancelUnifiedSearchFiles() + self.isSearchingMode = false self.literalSearch = "" self.providers?.removeAll() self.dataSource.removeAll() - self.reloadDataSource() + Task { + await self.reloadDataSource() + } + // + mainNavigationController?.hiddenPlusButton(false) } // MARK: - TAP EVENT - func tapMoreListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { - tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) - } - func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) } - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { - tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) } func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) { - if isEditMode { return } guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) @@ -1652,29 +1091,11 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS appDelegate.adjust.trackEvent(TriggerEvent(Sharing.rawValue)) } - func tapMoreGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { - if isEditMode { return } - guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } -// toggleMenu(metadata: metadata, image: image) - if namedButtonMore == NCGlobal.shared.buttonMoreMore || namedButtonMore == NCGlobal.shared.buttonMoreLock { - toggleMenu(metadata: metadata, image: image) - } else if namedButtonMore == NCGlobal.shared.buttonMoreStop { - Task { - await cancelSession(metadata: metadata) - } - } - } - func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } -// toggleMenu(metadata: metadata, image: image) - Task { - await cancelSession(metadata: metadata) - } + toggleMenu(metadata: metadata, image: image, sender: sender) } - func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func tapRichWorkspace(_ sender: Any) { if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { @@ -1688,34 +1109,12 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } - func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - // (+) - self.mainNavigationController?.menuPlus?.hiddenPlusButton(false) - - self.isSearchingMode = false - self.networkSearchInProgress = false - self.searchResultText = nil - self.searchResultStore = nil - - Task { - await searchOperationHandle.cancel() - await reloadDataSource() - - // Restore Layout - if let layoutForViewLayoutStore { - let layoutForView = database.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) - layoutForView.layout = layoutForViewLayoutStore - await setLayout(layoutForView: layoutForView) - } - layoutForViewLayoutStore = nil - - // update Option menu - await mainNavigationController?.updateMenuOption() - } + func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { + toggleMenu(metadata: metadata, image: image, sender: sender) } - func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?) { - toggleMenu(metadata: metadata, image: image) + func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { + unifiedSearchMore(metadataForSection: metadataForSection) } @MainActor @@ -1797,46 +1196,16 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS didSelectMetadata(metadata, withOcIds: false) } - func tapButtonSwitch(_ sender: Any) { - guard !isTransitioning else { return } - isTransitioning = true - - guard let layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) else { return } - - if layoutForView.layout == NCGlobal.shared.layoutGrid { - layoutForView.layout = NCGlobal.shared.layoutList - } else { - layoutForView.layout = NCGlobal.shared.layoutGrid - } - self.layoutForView = NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) - self.collectionView.reloadData() - self.collectionView.collectionViewLayout.invalidateLayout() - self.collectionView.setCollectionViewLayout(layoutForView.layout == NCGlobal.shared.layoutList ? self.listLayout : self.gridLayout, animated: true) {_ in self.isTransitioning = false } - } - - func tapButtonOrder(_ sender: Any) { - let sortMenu = NCSortMenu() - sortMenu.toggleMenu(viewController: self, account: session.account, key: layoutKey, sortButton: sender as? UIButton, serverUrl: serverUrl) - } - - func tapButtonTransfer(_ sender: Any) { } - - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } - func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func longPressListItem(with objectId: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) { - } - func longPressListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func longPressGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func longPressMoreListItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - - func longPressGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } - - func longPressMoreGridItem(with ocId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } - + + func longPressMoreGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) } @@ -1858,7 +1227,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS becomeFirstResponder() if !serverUrl.isEmpty { - listMenuItems.append(UIMenuItem(title: NSLocalizedString("_paste_file_", comment: ""), action: #selector(pasteFilesMenu))) + listMenuItems.append(UIMenuItem(title: NSLocalizedString("_paste_file_", comment: ""), action: #selector(pasteFilesMenu(_:)))) } if !listMenuItems.isEmpty { @@ -1867,23 +1236,16 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } - // MARK: - Transfer Delegate - - func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } - - func tranferChange(status: String, metadata: tableMetadata, error: NKError) { } - // MARK: - Menu Item override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - - if #selector(pasteFilesMenu) == action { + if #selector(pasteFilesMenu(_:)) == action { if !UIPasteboard.general.items.isEmpty, !(metadataFolder?.e2eEncrypted ?? false) { return true } - } else if #selector(copyMenuFile) == action { + } else if #selector(copyMenuFile(_:)) == action { return true - } else if #selector(moveMenuFile) == action { + } else if #selector(moveMenuFile(_:)) == action { return true } @@ -1987,20 +1349,36 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) } - await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: self.serverUrl) + DispatchQueue.main.async { +// UIView.transition(with: self.collectionView, +// duration: 0.20, +// options: .transitionCrossDissolve, +// animations: { self.collectionView.reloadData() }, +// completion: nil) +// +// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() + if isRecommendationActived { + Task.detached { + await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) + } + } } - await mainNavigationController?.updateMenuOption() - (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + DispatchQueue.main.async { +// UIView.transition(with: self.collectionView, +// duration: 0.20, +// options: .transitionCrossDissolve, +// animations: { self.collectionView.reloadData() }, +// completion: nil) +// +// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() self.refreshControl.endRefreshing() self.collectionView.reloadData() self.setNavigationRightItems() } } - func getServerData() { - } + func getServerData(forced: Bool = false) async { } @objc func networkSearch() { guard !networkSearchInProgress else { @@ -2051,8 +1429,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } completion: { metadatasSearch, error in Task { guard let metadatasSearch, - error == .success, - self.isSearchingMode + error == .success, + self.isSearchingMode else { self.networkSearchInProgress = false await self.reloadDataSource() @@ -2079,11 +1457,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS guard let metadataForSection = metadataForSection, let lastSearchResult = metadataForSection.lastSearchResult, let cursor = lastSearchResult.cursor, let term = literalSearch else { return } metadataForSection.unifiedSearchInProgress = true - Task { - await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: nil) - } - } + self.collectionView?.reloadData() self.networking.unifiedSearchFilesProvider(id: lastSearchResult.id, term: term, limit: 5, cursor: cursor, account: session.account) { task in self.searchDataSourceTask = task @@ -2092,8 +1466,12 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } completion: { _, searchResult, metadatas, error in if error != .success { - Task { - await showErrorBanner(controller: self.controller, text: error.errorDescription) + Task {@MainActor in + await showErrorBanner( + controller: self.controller, + errorDescription: error.errorDescription, + errorCode: error.errorCode + ) } } @@ -2101,10 +1479,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS guard let searchResult = searchResult, let metadatas = metadatas else { return } self.dataSource.appendMetadatasToSection(metadatas, metadataForSection: metadataForSection, lastSearchResult: searchResult) - Task { - await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: nil) - } + DispatchQueue.main.async { + self.collectionView?.reloadData() } } } @@ -2136,13 +1512,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } } - - func pushViewController(viewController: UIViewController) { - if pushed { return } - - pushed = true - navigationController?.pushViewController(viewController, animated: true) - } func pushViewController(viewController: UIViewController) { if pushed { return } @@ -2160,7 +1529,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS !results.isEmpty { return Array(results) } - return nil } func sizeForHeaderInSection(section: Int) -> CGSize { @@ -2169,12 +1537,12 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let isIphone = UIDevice.current.userInterfaceIdiom == .phone if self.dataSource.isEmpty() { - height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset, isHeaderMenuTransferViewEnabled: isHeaderMenuTransferViewEnabled() != nil) + height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset) } else if isEditMode || (isLandscape && isIphone) { return CGSize.zero } else { - let (heightHeaderCommands, heightHeaderRichWorkspace, heightHeaderSection) = getHeaderHeight(section: section) - height = heightHeaderCommands + heightHeaderRichWorkspace + heightHeaderSection + let (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) = getHeaderHeight(section: section) + height = heightHeaderRichWorkspace + heightHeaderRecommendations + heightHeaderSection } return CGSize(width: collectionView.frame.width, height: height) @@ -2183,14 +1551,23 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // MARK: - Footer size func sizeForFooterInSection(section: Int) -> CGSize { + guard let controller else { + return CGSize.zero + } let sections = dataSource.numberOfSections() - let metadataForSection = self.dataSource.getMetadataForSection(section) - let isPaginated = metadataForSection?.lastSearchResult?.isPaginated ?? false - let metadatasCount: Int = metadataForSection?.lastSearchResult?.entries.count ?? 0 - var size = CGSize(width: collectionView.frame.width, height: 0) + let bottomAreaInsets: CGFloat = controller.tabBar.safeAreaInsets.bottom == 0 ? 34 : 0 + let height = controller.tabBar.frame.height + bottomAreaInsets + + if isEditMode { + return CGSize(width: collectionView.frame.width, height: 90 + height) + } + + if isSearchingMode { + return CGSize(width: collectionView.frame.width, height: 50) + } if section == sections - 1 { - size.height += 85 + return CGSize(width: collectionView.frame.width, height: height) } else { size.height += 1 } diff --git a/iOSClient/NCBackgroundLocationUploadManager.swift b/iOSClient/NCBackgroundLocationUploadManager.swift index 154fda662d..8f63b82e9d 100644 --- a/iOSClient/NCBackgroundLocationUploadManager.swift +++ b/iOSClient/NCBackgroundLocationUploadManager.swift @@ -1,6 +1,10 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2025 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCBackgroundLocationUploadManager.swift +// Nextcloud +// +// Created by Marino Faggiana on 06/06/25. +// Copyright © 2025 Marino Faggiana. All rights reserved. +// import CoreLocation import NextcloudKit @@ -102,16 +106,15 @@ class NCBackgroundLocationUploadManager: NSObject, CLLocationManagerDelegate { } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - // Must work only in background guard isAppInBackground else { return } // Open Realm - guard NCManageDatabase.shared.openRealmBackground() else { - nkLog(tag: self.global.logTagLocation, emoji: .error, message: "Failed to open Realm in Location Manager") - return - } + if database.openRealmBackground() { + let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! + let location = locations.last + nkLog(tag: self.global.logTagLocation, emoji: .start, message: "Triggered by location change: \(location?.coordinate.latitude ?? 0), \(location?.coordinate.longitude ?? 0)") let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! let location = locations.last diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index 2fcf152ceb..3cb1bc1ec6 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -50,7 +50,7 @@ final class NCGlobal: Sendable { // let avatarSize: Int = 128 * Int(UIScreen.main.scale) let avatarSizeRounded: Int = 128 - + // Preview size // let size1024: CGSize = CGSize(width: 1024, height: 1024) @@ -229,7 +229,6 @@ final class NCGlobal: Sendable { let selectorSynchronizationOffline = "synchronizationOffline" let selectorPrint = "print" let selectorDeleteFile = "deleteFile" - // Metadata : Status // // 0 normal @@ -259,7 +258,6 @@ final class NCGlobal: Sendable { let metadataStatusForScreenAwake = [-1, -2, 1, 2] let metadataStatusHideInView = [1, 2, 3, 11] let metadataStatusWaitWebDav = [10, 11, 12, 13, 14, 15] - let metadataStatusTransfers = [-2, -3, 2, 3, 10, 11, 12, 13, 14, 15] let metadatasStatusInWaiting = [-1, 1, 10, 11, 12, 13, 14, 15] let metadatasStatusInProgress = [-2, 2] @@ -276,6 +274,8 @@ final class NCGlobal: Sendable { let notificationCenterChangeTheming = "changeTheming" // userInfo: account let notificationCenterRichdocumentGrabFocus = "richdocumentGrabFocus" let notificationCenterReloadDataNCShare = "reloadDataNCShare" + let notificationCenterDidCreateShareLink = "didCreateShareLink" + let notificationCenterCloseRichWorkspaceWebView = "closeRichWorkspaceWebView" let notificationCenterReloadAvatar = "reloadAvatar" let notificationCenterClearCache = "clearCache" @@ -293,6 +293,8 @@ final class NCGlobal: Sendable { let notificationCenterFileExists = "fileExists" // userInfo: ocId, fileExists let notificationCenterReloadDataSource = "reloadDataSource" // userInfo: serverUrl?, clearDataSource + let notificationCenterRenameFile = "renameFile" // userInfo: serverUrl, account, error + let notificationCenterMenuSearchTextPDF = "menuSearchTextPDF" let notificationCenterMenuGotToPageInPDF = "menuGotToPageInPDF" @@ -424,7 +426,7 @@ final class NCGlobal: Sendable { let keyFileNameAutoUploadType = "fileNameAutoUploadType" let keyFileNameOriginal = "fileNameOriginal" let keyFileNameOriginalAutoUpload = "fileNameOriginalAutoUpload" - + // LOG TAG // let logTagTask = "BGT" diff --git a/iOSClient/Networking/NCConfigServer.swift b/iOSClient/Networking/NCConfigServer.swift index f82b466647..d5e4d2a867 100644 --- a/iOSClient/Networking/NCConfigServer.swift +++ b/iOSClient/Networking/NCConfigServer.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2022 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCConfigServer.swift +// Nextcloud +// +// Created by Marino Faggiana on 05/12/22. +// Copyright © 2022 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import Foundation import UIKit diff --git a/iOSClient/Networking/NCNetworking+Download.swift b/iOSClient/Networking/NCNetworking+Download.swift index 4d9fe0c51c..22ecb91653 100644 --- a/iOSClient/Networking/NCNetworking+Download.swift +++ b/iOSClient/Networking/NCNetworking+Download.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCNetworking+Download.swift +// Nextcloud +// +// Created by Marino Faggiana on 07/02/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import UIKit import NextcloudKit @@ -327,3 +346,40 @@ extension NCNetworking { } } } + +class NCOperationDownload: ConcurrentOperation, @unchecked Sendable { + var metadata: tableMetadata + var selector: String + + init(metadata: tableMetadata, selector: String) { + self.metadata = tableMetadata.init(value: metadata) + self.selector = selector + } + + override func start() { + guard !isCancelled else { return self.finish() } + + metadata.session = NCNetworking.shared.sessionDownload + metadata.sessionError = "" + metadata.sessionSelector = selector + metadata.sessionTaskIdentifier = 0 + metadata.status = NCGlobal.shared.metadataStatusWaitDownload + +// let metadata = NCManageDatabase.shared.addMetadata(metadata) + +// NCNetworking.shared.download(metadata: metadata, withNotificationProgressTask: true) { +// } completion: { _, _ in +// self.finish() +// } + Task { + await download(withSelector: self.selector) + } + } + + private func download(withSelector selector: String = "") async { + await NCNetworking.shared.downloadFile(metadata: metadata) { _ in + self.finish() + } taskHandler: { _ in } + + } +} diff --git a/iOSClient/Networking/NCNetworking+LivePhoto.swift b/iOSClient/Networking/NCNetworking+LivePhoto.swift index e861b76b81..072b3b087f 100644 --- a/iOSClient/Networking/NCNetworking+LivePhoto.swift +++ b/iOSClient/Networking/NCNetworking+LivePhoto.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCNetworking+LivePhoto.swift +// Nextcloud +// +// Created by Marino Faggiana on 07/02/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import UIKit import NextcloudKit diff --git a/iOSClient/Networking/NCNetworking+Task.swift b/iOSClient/Networking/NCNetworking+Task.swift index f28830b583..1b6122252b 100644 --- a/iOSClient/Networking/NCNetworking+Task.swift +++ b/iOSClient/Networking/NCNetworking+Task.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCNetworking+Task.swift +// Nextcloud +// +// Created by Marino Faggiana on 24/08/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import Foundation import UIKit diff --git a/iOSClient/Networking/NCNetworking+Upload.swift b/iOSClient/Networking/NCNetworking+Upload.swift index 9467e9c0b7..6c985dbe7c 100644 --- a/iOSClient/Networking/NCNetworking+Upload.swift +++ b/iOSClient/Networking/NCNetworking+Upload.swift @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later - import UIKit import NextcloudKit import Alamofire @@ -45,6 +41,22 @@ extension NCNetworking { } taskHandler(task) } progressHandler: { progress in + Task { + guard await self.progressQuantizer.shouldEmit(serverUrlFileName: serverUrlFileName, fraction: progress.fractionCompleted) else { + return + } + + if let metadata { + await NCManageDatabase.shared.setMetadataProgress(ocId: metadata.ocId, progress: progress.fractionCompleted) + await self.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferProgressDidUpdate(progress: Float(progress.fractionCompleted), + totalBytes: progress.totalUnitCount, + totalBytesExpected: progress.completedUnitCount, + fileName: metadata.fileName, + serverUrl: metadata.serverUrl) + } + } + } progressHandler(progress.completedUnitCount, progress.totalUnitCount, progress.fractionCompleted) } @@ -378,6 +390,7 @@ extension NCNetworking { error: self.global.diagnosticProblemsUploadServerError) } } + await NCManageDatabase.shared.updateBadge() } // MARK: - diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 37629dadd1..4d78a1844e 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCNetworking+WebDAV.swift +// Nextcloud +// +// Created by Marino Faggiana on 07/02/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import UIKit import NextcloudKit @@ -465,6 +484,50 @@ extension NCNetworking { return .success } + + func renameMetadata(_ metadata: tableMetadata, + fileNameNew: String, + indexPath: IndexPath, + viewController: UIViewController?, + completion: @escaping (_ error: NKError) -> Void) { + + let permission = NCMetadataPermissions.permissionsContainsString(metadata.permissions, permissions: NCMetadataPermissions.permissionCanRename) + if (!metadata.permissions.isEmpty && permission == false) || + (metadata.status != global.metadataStatusNormal && metadata.status != global.metadataStatusWaitRename) { +// return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_no_permission_modify_file_")) +// let error = await NCNetworkingE2EERename().rename(metadata: metadata, fileNameNew: fileNameNew) +// DispatchQueue.main.async { completion(error) } + completion(NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_no_permission_modify_file_")) + } + + if metadata.isDirectoryE2EE { +#if !EXTENSION + if isOffline { +// return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) + completion(NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) + } + Task { + let error = await NCNetworkingE2EERename().rename(metadata: metadata, fileNameNew: fileNameNew) + if error != .success { +// NCContentPresenter().showError(error: error) + completion(error) + } + } +#endif + } else { + Task { + await self.transferDispatcher.notifyAllDelegatesAsync { delegate in + let status = self.global.metadataStatusWaitRename + await NCManageDatabase.shared.renameMetadata(fileNameNew: fileNameNew, ocId: metadata.ocId, status: status) + delegate.transferReloadData(serverUrl: metadata.serverUrl, status: status) + } + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterRenameFile, userInfo: ["serverUrl": metadata.serverUrl, "account": metadata.account, "error": NKError(errorCode: 0, errorDescription: ""), "ocId": metadata.ocId, "indexPath": indexPath]) + + } + + completion(NKError(errorCode: 0, errorDescription: "")) + } + } func renameFileOrFolder(metadata: tableMetadata) async -> NKError { let serverUrlFileNameSource = metadata.serverUrlFileName diff --git a/iOSClient/Networking/NCNetworking.swift b/iOSClient/Networking/NCNetworking.swift index c0684756d4..0c86cf84c8 100644 --- a/iOSClient/Networking/NCNetworking.swift +++ b/iOSClient/Networking/NCNetworking.swift @@ -37,6 +37,203 @@ protocol NCTransferDelegate: AnyObject { serverUrl: String) } +extension NCTransferDelegate { + func transferChange(status: String, + account: String, + fileName: String, + serverUrl: String, + selector: String?, + ocId: String, + destination: String?, + error: NKError) {} + func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) {} + func transferProgressDidUpdate(progress: Float, + totalBytes: Int64, + totalBytesExpected: Int64, + fileName: String, + serverUrl: String) {} +} + +/// Actor-based delegate dispatcher using weak references. +actor NCTransferDelegateDispatcher { + // Weak reference collection of delegates + private var transferDelegates = NSHashTable.weakObjects() + + /// Adds a delegate safely. + func addDelegate(_ delegate: NCTransferDelegate) { + transferDelegates.add(delegate) + } + + /// Remove a delegate safely. + func removeDelegate(_ delegate: NCTransferDelegate) { + transferDelegates.remove(delegate) + } + + /// Notifies all delegates. + func notifyAllDelegates(_ block: (NCTransferDelegate) -> Void) { + let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + for delegate in delegatesCopy { + block(delegate) + } + } + + func notifyAllDelegatesAsync(_ block: @escaping (NCTransferDelegate) async -> Void) async { + let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + for delegate in delegatesCopy { + await block(delegate) + } + } + + /// Notifies the delegate for a specific scene. + func notifyDelegate(forScene sceneIdentifier: String, _ block: (NCTransferDelegate) -> Void) { + let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + for delegate in delegatesCopy { + if delegate.sceneIdentifier == sceneIdentifier { + block(delegate) + } + } + } + + /// Notifies matching and non-matching delegates for a specific scene. + func notifyDelegates(forScene sceneIdentifier: String, + matching: (NCTransferDelegate) -> Void, + others: (NCTransferDelegate) -> Void) { + let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + for delegate in delegatesCopy { + if delegate.sceneIdentifier == sceneIdentifier { + matching(delegate) + } else { + others(delegate) + } + } + } +} + +/// A thread-safe registry for tracking in-flight `URLSessionTask` instances. +/// +/// Each task is associated with a string identifier (`identifier`) that you define, +/// allowing you to check whether a request is already running, avoid duplicates, +/// and cancel all active tasks at once. The registry automatically removes +/// completed tasks via `cleanupCompleted()` to keep memory usage compact. +/// +/// Typical use cases: +/// - Ensure only one task per identifier is active at a time. +/// - Query whether a specific request is still running (`isReading`). +/// - Forcefully stop a specific request (`cancel`). +/// - Forcefully stop all tasks when leaving a screen (`cancelAll`). +actor NetworkingTasks { + private var active: [(identifier: String, task: URLSessionTask)] = [] + + /// Returns whether there is an in-flight task for the given URL. + /// + /// A task is considered in-flight if its `state` is `.running` or `.suspended`. + /// - Parameter identifier: The identifier to check. + /// - Returns: `true` if a matching in-flight task exists; otherwise `false`. + func isReading(identifier: String) -> Bool { + // Drop finished/canceling tasks globally + cleanup() + + return active.contains { + $0.identifier == identifier && ($0.task.state == .running || $0.task.state == .suspended) + } + } + + /// Tracks a newly created `URLSessionTask` for the given identifier. + /// + /// If a running entry for the same identifier exists, it is removed before appending the new one. + /// - Parameters: + /// - identifier: The identifier associated with the task. + /// - task: The `URLSessionTask` to track. + func track(identifier: String, task: URLSessionTask) { + // Drop finished/canceling tasks globally + cleanup() + + active.removeAll { + $0.identifier == identifier && $0.task.state == .running + } + active.append((identifier, task)) + nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .start, message: "Start task for identifier: \(identifier)", consoleOnly: true) + } + + /// create a Identifier + /// + func createIdentifier(account: String? = nil, path: String? = nil, name: String) -> String { + if let account, + let path { + return account + "_" + path + "_" + name + } else if let path { + return path + "_" + name + } else { + return name + } + } + + /// Cancels and removes all tasks associated with the given id. + /// + /// - Parameter identifier: The identifier whose tasks should be canceled. + func cancel(identifier: String) { + // Drop finished/canceling tasks globally + cleanup() + + for element in active where element.identifier == identifier { + element.task.cancel() + nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .cancel, message: "Cancel task for identifier: \(identifier)", consoleOnly: true) + } + active.removeAll { + $0.identifier == identifier + } + } + + /// Cancels all tracked `URLSessionTask` and clears the registry. + /// + /// Call this when leaving the page/screen or when the operation must be forcefully stopped. + func cancelAll() { + active.forEach { + $0.task.cancel() + nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .cancel, message: "Cancel task with identifier: \($0.identifier)", consoleOnly: true) + } + active.removeAll() + } + + /// Removes tasks that have completed from the registry. + /// + /// Useful to keep the in-memory list compact during long-running operations. + func cleanup() { + active.removeAll { + $0.task.state == .completed || $0.task.state == .canceling + } + } +} + +/// Quantizes per-task progress updates to integer percentages (0...100). +/// Each (serverUrlFileName) pair is tracked separately, so you get +/// at most one update per integer percent for each transfer. +actor ProgressQuantizer { + private var lastPercent: [String: Int] = [:] + + /// Returns `true` only when integer percent changes (or hits 100). + /// + /// - Parameters: + /// - serverUrlFileName: The name of the file being transferred. + /// - fraction: Progress fraction [0.0 ... 1.0]. + func shouldEmit(serverUrlFileName: String, fraction: Double) -> Bool { + let percent = min(max(Int((fraction * 100).rounded(.down)), 0), 100) + + let last = lastPercent[serverUrlFileName] ?? -1 + guard percent != last || percent == 100 else { + return false + } + + lastPercent[serverUrlFileName] = percent + return true + } + + /// Clears stored state for a finished transfer. + func clear(serverUrlFileName: String) { + lastPercent.removeValue(forKey: serverUrlFileName) + } +} + class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { static let shared = NCNetworking() @@ -94,6 +291,7 @@ class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { let downloadAvatarQueue = Queuer(name: "downloadAvatarQueue", maxConcurrentOperationCount: 10, qualityOfService: .default) let fileExistsQueue = Queuer(name: "fileExistsQueue", maxConcurrentOperationCount: 10, qualityOfService: .default) #endif + let downloadQueue = Queuer(name: "downloadQueue", maxConcurrentOperationCount: NCBrandOptions.shared.httpMaximumConnectionsPerHostInDownload, qualityOfService: .default) // MARK: - init diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index 930a965320..9121911b0b 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -271,7 +271,6 @@ actor NCNetworkingProcess { await startTimer(interval: minInterval) } } else { - // Remove upload asset await removeUploadedAssetsIfNeeded() // Set Live Photo diff --git a/iOSClient/Networking/NCService.swift b/iOSClient/Networking/NCService.swift index f6715693d3..2987e2543c 100644 --- a/iOSClient/Networking/NCService.swift +++ b/iOSClient/Networking/NCService.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2018 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCService.swift +// Nextcloud +// +// Created by Marino Faggiana on 14/03/18. +// Copyright © 2018 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import UIKit @preconcurrency import NextcloudKit diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index cd2a633acc..989135a5fe 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -232,8 +232,6 @@ "_settings_account_request_" = "Request account at startup"; "_print_" = "Print"; "_alias_" = "Alias"; -//"_alias_placeholder_" = "Write alias"; -//"_alias_footer_" = "Give your accounts a descriptive name such as Home, Office, School…"; "_alias_placeholder_" = "Write the alias"; "_alias_footer_" = "Give your account names a descriptive name such as Home, Office, School …"; "_chunk_size_mb_" = "Chunk size in MB"; @@ -741,7 +739,6 @@ "_share_link_" = "Share link"; //"_share_link_" = "Share link"; "_share_link_" = "Link"; -//"_share_link_" = "Share link"; "_share_link_button_" = "Send link to …"; "_share_link_name_" = "Link name"; "_password_" = "Password"; @@ -1142,9 +1139,6 @@ "_voice_memo_stop_" = "Tap to stop"; "_voice_memo_filename_" = "Voice memo"; "_voice_memo_title_" = "Upload voice memo"; -//"Enter Passcode" = "Enter Passcode"; -//"Enter a new passcode" = "Enter a new passcode"; -//"Confirm new passcode" = "Confirm new passcode"; "_too_many_failed_passcode_attempts_error_" = "Too many failed passcode attempts. Please try again."; //"Passcodes didn't match. Try again." = "Passcodes didn't match. Try again."; "Delete" = "Delete"; From 49b64df3dd898bfb3f783ee3d88fe8ef995639fb Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Thu, 9 Apr 2026 18:59:22 +0530 Subject: [PATCH 31/40] NMC 1984 - Privacy policy customisation updated changes with 10.3.0 version --- .../Data/NCManageDatabase+LocalFile.swift | 40 +- .../Data/NCManageDatabase+Metadata.swift | 66 -- .../NCCollectionViewCommon+SyncMetadata.swift | 14 +- .../NCCollectionViewCommon.swift | 637 ++++++------------ .../NCBackgroundLocationUploadManager.swift | 19 +- iOSClient/NCGlobal.swift | 10 +- iOSClient/Networking/NCConfigServer.swift | 25 +- .../Networking/NCNetworking+Download.swift | 62 +- .../Networking/NCNetworking+LivePhoto.swift | 25 +- iOSClient/Networking/NCNetworking+Task.swift | 25 +- .../Networking/NCNetworking+Upload.swift | 21 +- .../Networking/NCNetworking+WebDAV.swift | 69 +- iOSClient/Networking/NCNetworking.swift | 198 ------ .../Networking/NCNetworkingProcess.swift | 1 + iOSClient/Networking/NCService.swift | 25 +- .../PrivacySettingsViewController.swift | 9 +- .../en.lproj/Localizable.strings | 3 + 17 files changed, 274 insertions(+), 975 deletions(-) diff --git a/iOSClient/Data/NCManageDatabase+LocalFile.swift b/iOSClient/Data/NCManageDatabase+LocalFile.swift index 334e5fba25..2146dec1ad 100644 --- a/iOSClient/Data/NCManageDatabase+LocalFile.swift +++ b/iOSClient/Data/NCManageDatabase+LocalFile.swift @@ -29,17 +29,14 @@ extension NCManageDatabase { // MARK: - Realm Write + /// Adds or updates multiple local file entries corresponding to the given metadata array. + /// Uses async Realm read + single write transaction. Assumes `tableLocalFile` has a primary key. /// - Parameters: - /// - metadata: The `tableMetadata` containing file details. - /// - offline: Optional flag to mark the file as available offline. - /// - Returns: Nothing. Realm write is performed asynchronously. - func addLocalFileAsync(metadata: tableMetadata, offline: Bool? = nil) async { - // Read (non-blocking): safely detach from Realm thread - let existing: tableLocalFile? = performRealmRead { realm in - realm.objects(tableLocalFile.self) - .filter(NSPredicate(format: "ocId == %@", metadata.ocId)) - .first - .map { tableLocalFile(value: $0) } + /// - metadatas: Array of `tableMetadata` to map into `tableLocalFile`. + /// - offline: Optional override for the `offline` flag applied to all items. + func addLocalFilesAsync(metadatas: [tableMetadata], offline: Bool? = nil) async { + guard !metadatas.isEmpty else { + return } // Extract ocIds for efficient lookup @@ -62,19 +59,20 @@ extension NCManageDatabase { // Reuse existing object or create a new one let local = existingMap[metadata.ocId] ?? tableLocalFile() - addObject.account = metadata.account - addObject.etag = metadata.etag - addObject.exifDate = NSDate() - addObject.exifLatitude = "-1" - addObject.exifLongitude = "-1" - addObject.ocId = metadata.ocId - addObject.fileName = metadata.fileName + local.account = metadata.account + local.etag = metadata.etag + local.exifDate = NSDate() + local.exifLatitude = "-1" + local.exifLongitude = "-1" + local.ocId = metadata.ocId + local.fileName = metadata.fileName - if let offline { - addObject.offline = offline - } + if let offline { + local.offline = offline + } - realm.add(addObject, update: .all) + realm.add(local, update: .all) + } } } diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index 5c3b840691..67ed7ad2f2 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -151,37 +151,10 @@ extension tableMetadata { (fileNameView as NSString).deletingPathExtension } - var isRenameable: Bool { - if !NCMetadataPermissions.canRename(self) { - return false - } - if lock { - return false - } - if !isDirectoryE2EE && e2eEncrypted { - return false - } - return true - } - - var isPrintable: Bool { - if isDocumentViewableOnly { - return false - } - if ["application/pdf", "com.adobe.pdf"].contains(contentType) || contentType.hasPrefix("text/") || classFile == NKTypeClassFile.image.rawValue { - return true - } - return false - } - var isSavebleInCameraRoll: Bool { return (classFile == NKTypeClassFile.image.rawValue && contentType != "image/svg+xml") || classFile == NKTypeClassFile.video.rawValue } - var isDocumentViewableOnly: Bool { - sharePermissionsCollaborationServices == NCPermissions().permissionReadShare && classFile == NKTypeClassFile.document.rawValue - } - var isAudioOrVideo: Bool { return classFile == NKTypeClassFile.audio.rawValue || classFile == NKTypeClassFile.video.rawValue } @@ -371,17 +344,6 @@ extension tableMetadata { return !lock || (lockOwner == user && lockOwnerType == 0) } - // Return if is sharable - func isSharable() -> Bool { - guard let capabilities = NCNetworking.shared.capabilities[account] else { - return false - } - if !capabilities.fileSharingApiEnabled || (capabilities.e2EEEnabled && isDirectoryE2EE), !e2eEncrypted { - return false - } - return !e2eEncrypted - } - /// Returns a detached (unmanaged) deep copy of the current `tableMetadata` object. /// /// - Note: Primitive properties and lists of primitive values (for example `shareType`) @@ -969,34 +931,6 @@ extension NCManageDatabase { .map { $0.detachedCopy() } } ?? [] } - - func getMediaMetadatas(predicate: NSPredicate, sorted: String? = nil, ascending: Bool = false) -> ThreadSafeArray? { - - do { - let realm = try Realm() - if let sorted { - var results: [tableMetadata] = [] - switch sorted {//NCPreferences().mediaSortDate { - case "date": - results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.date as Date) > ($1.date as Date) } - case "creationDate": - results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.creationDate as Date) > ($1.creationDate as Date) } - case "uploadDate": - results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.uploadDate as Date) > ($1.uploadDate as Date) } - default: - let results = realm.objects(tableMetadata.self).filter(predicate) - return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) - } - return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) - } else { - let results = realm.objects(tableMetadata.self).filter(predicate) - return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) - } - } catch let error as NSError { -// NextcloudKit.shared.nkCommonInstance.writeLog("Could not access database: \(error)") - } - return nil - } func getMetadatas(predicate: NSPredicate, sortedByKeyPath: String, diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift index 571306d2de..5e0254858d 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift @@ -21,7 +21,7 @@ extension NCCollectionViewCommon { // If a sync task is already running, do not start a new one if let task = syncMetadatasTask, !task.isCancelled { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .info, message: "Exit: Another sync is already running. Skipping this one.", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .info, message: "Exit: Another sync is already running. Skipping this one.", consoleOnly: true) return } @@ -45,9 +45,9 @@ extension NCCollectionViewCommon { func stopSyncMetadata() async { if let task = syncMetadatasTask { if task.isCancelled { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .stop, message: "Sync Metadata for \(self.serverUrl) was already cancelled.", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .stop, message: "Sync Metadata for \(self.serverUrl) was already cancelled.", consoleOnly: true) } else { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .stop, message: "Stopping active Sync Metadata for \(self.serverUrl).", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .stop, message: "Stopping active Sync Metadata for \(self.serverUrl).", consoleOnly: true) } } @@ -74,7 +74,7 @@ extension NCCollectionViewCommon { return } let identifier = self.serverUrl + "_syncMetadata" - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .start, message: "Start Sync Metadata for \(self.serverUrl)") + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .start, message: "Start Sync Metadata for \(self.serverUrl)") // Always cancel and clear all tracked URLSessionTask on any exit path defer { @@ -85,7 +85,7 @@ extension NCCollectionViewCommon { // If a readFile for this serverUrl is already in-flight, do nothing if await networking.networkingTasks.isReading(identifier: identifier) { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .debug, message: "ReadFile for this \(self.serverUrl) is already in-flight.", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .debug, message: "ReadFile for this \(self.serverUrl) is already in-flight.", consoleOnly: true) return } @@ -151,9 +151,9 @@ extension NCCollectionViewCommon { // If this folder failed, skip it but keep processing others if resultsReadFolder.error == .success { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .network, message: "Read correctly: \(serverUrl)", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .network, message: "Read correctly: \(serverUrl)", consoleOnly: true) } else { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .error, message: "Read failed for \(serverUrl) with error: \(resultsReadFolder.error.errorDescription)") + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .error, message: "Read failed for \(serverUrl) with error: \(resultsReadFolder.error.errorDescription)") return } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index c502da76a7..8cb4844927 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -4,14 +4,12 @@ import UIKit import SwiftUI -import Realm import RealmSwift import NextcloudKit import EasyTipView import LucidBanner -import MoEngageInApps -class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionFirstHeaderDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { +class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { @IBOutlet weak var collectionView: UICollectionView! @@ -54,12 +52,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var tabBarSelect: NCCollectionViewCommonSelectTabBar? var attributesZoomIn: UIMenuElement.Attributes = [] var attributesZoomOut: UIMenuElement.Attributes = [] - var syncMetadatasTask: Task? - let maxImageGrid: CGFloat = 7 -// var headerMenu: NCSectionFirstHeader? - var tipViewAccounts: EasyTipView? - var selectableDataSource: [RealmSwiftObject] { dataSource.getMetadataSourceForAllSections() } + var syncMetadatasTask: Task? // DECLARE var layoutKey = "" @@ -167,7 +161,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return self.serverUrl == self.utilityFileSystem.getHomeServer(session: self.session) && capabilities.recommendations } - internal let debouncer = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerReloadDataSource = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerReloadData = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerGetServerData = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerNetworkSearch = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) // MARK: - View Life Cycle @@ -276,11 +273,21 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.sectionFirstHeader?.setRichWorkspaceColor(style: view.traitCollection.userInterfaceStyle) } - NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterChangeTheming), object: nil, queue: .main) { [weak self] _ in - guard let self else { return } - self.collectionView.reloadData() + NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterChangeTheming), object: nil, queue: .main) { _ in + let serverUrl = self.serverUrl + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: serverUrl) + } + } } - + + NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterUserInteractionMonitor), object: nil, queue: .main) { _ in + Task { + await self.debouncerReloadData.resume() + } + } + NotificationCenter.default.addObserver(self, selector: #selector(updateIcons), name: NSNotification.Name(rawValue: global.notificationCenterUpdateIcons), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateIcons), name: NSNotification.Name(rawValue: global.notificationCenterUpdateIcons), object: nil) @@ -293,6 +300,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) +// AnalyticsHelper.shared.displayInAppNotification() + if titlePreviusFolder != nil { navigationController?.navigationBar.topItem?.title = titlePreviusFolder } @@ -305,6 +314,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS isEditMode = false Task { + await NCNetworking.shared.transferDispatcher.addDelegate(self) + await (self.navigationController as? NCMainNavigationController)?.setNavigationLeftItems() await (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() } @@ -399,7 +410,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } @objc func updateIcons() { - collectionView.reloadData() +// collectionView.reloadData() + Task { + await self.reloadDataSource() + } } func isApplicationUpdated() -> Bool { @@ -415,287 +429,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.present(newViewController!, animated: true, completion: nil) } - // MARK: - Transfer Delegate - - func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } - - func transferChange(status: String, metadatasError: [tableMetadata: NKError]) { - switch status { - // DELETE - case self.global.networkingStatusDelete: - let errorForThisServer = metadatasError.first { entry in - let (key, value) = entry - return key.serverUrl == self.serverUrl && value != .success - }?.value - - let needLoadDataSource = metadatasError.contains { entry in - let (key, value) = entry - return key.serverUrl == self.serverUrl && value == .success - } - - if let error = errorForThisServer { - NCContentPresenter().showError(error: error) - } - - if self.isSearchingMode { - self.networkSearch() - } else if needLoadDataSource { - Task { - await self.reloadDataSource() - } - } else { - Task.detached { - if await self.isRecommendationActived() { - await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) - } - } - } - default: - break - } - } - - func transferChange(status: String, metadata: tableMetadata, error: NKError) { - guard session.account == metadata.account else { return } - - if error != .success { - NCContentPresenter().showError(error: error) - } - - DispatchQueue.main.async { - switch status { - // UPLOADED, UPLOADED LIVEPHOTO - case self.global.networkingStatusUploaded, self.global.networkingStatusUploadedLivePhoto: - self.debouncer.call { - if self.isSearchingMode { - self.networkSearch() - } else if self.serverUrl == metadata.serverUrl { - Task { - await self.reloadDataSource() - } - } - } - // DOWNLOAD - case self.global.networkingStatusDownloading: - Task { - if metadata.serverUrl == self.serverUrl { - await self.reloadDataSource() - } - } - case self.global.networkingStatusDownloaded: - Task { - if metadata.serverUrl == self.serverUrl { - await self.reloadDataSource() - } - } - case self.global.networkingStatusDownloadCancel: - Task { - if metadata.serverUrl == self.serverUrl { - await self.reloadDataSource() - } - } - // CREATE FOLDER - case self.global.networkingStatusCreateFolder: - if metadata.serverUrl == self.serverUrl, metadata.sessionSelector != self.global.selectorUploadAutoUpload { - self.pushMetadata(metadata) - } - // RENAME - case self.global.networkingStatusRename: - self.debouncer.call { - if self.isSearchingMode { - self.networkSearch() - } else if self.serverUrl == metadata.serverUrl { - Task { - await self.reloadDataSource() - } - } - } - // FAVORITE - case self.global.networkingStatusFavorite: - self.debouncer.call { - if self.isSearchingMode { - self.networkSearch() - } else if self is NCFavorite { - Task { - await self.reloadDataSource() - } - } else if self.serverUrl == metadata.serverUrl { - Task { - await self.reloadDataSource() - } - } - } - default: - break - } - } - } - - func transferReloadData(serverUrl: String?, status: Int?) { - self.debouncer.call { - if self.isSearchingMode { - guard status != self.global.metadataStatusWaitDelete, - status != self.global.metadataStatusWaitRename, - status != self.global.metadataStatusWaitMove, - status != self.global.metadataStatusWaitCopy, - status != self.global.metadataStatusWaitFavorite else { - return - } - self.networkSearch() - } else if ( self.serverUrl == serverUrl) || serverUrl == nil { - Task { - await self.reloadDataSource() - } - } - } - } - - func transferRequestData(serverUrl: String?) { - self.debouncer.call { - if self.isSearchingMode { - self.networkSearch() - } else if ( self.serverUrl == serverUrl) || serverUrl == nil { - Task { - await self.getServerData() - } - } - } - } - - func transferCopy(metadata: tableMetadata, destination: String, error: NKError) { - if error != .success { - NCContentPresenter().showError(error: error) - } - - if isSearchingMode { - return networkSearch() - } - - if metadata.serverUrl == self.serverUrl || destination == self.serverUrl { - Task { - await self.reloadDataSource() - } - } - } - - func transferMove(metadata: tableMetadata, destination: String, error: NKError) { - if error != .success { - NCContentPresenter().showError(error: error) - } - - if isSearchingMode { - return networkSearch() - } - - if metadata.serverUrl == self.serverUrl || destination == self.serverUrl { - Task { - await self.reloadDataSource() - } - } - } - - func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } - - func transferChange(status: String, - account: String, - fileName: String, - serverUrl: String, - selector: String?, - ocId: String, - destination: String?, - error: NKError) { - Task { - if error != .success, - error.errorCode != global.errorResourceNotFound { - await showErrorBanner(controller: self.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode) - } - guard session.account == account else { - return - } - - await self.debouncer.call { - switch status { - // UPLOADED, UPLOADED LIVEPHOTO, DELETE - case self.global.networkingStatusUploaded, - self.global.networkingStatusDelete, - self.global.networkingStatusCopyMove: - if self.isSearchingMode { - self.networkSearch() - } else if self.serverUrl == serverUrl || destination == self.serverUrl { - await self.reloadDataSource() - } - // DOWNLOAD - case self.global.networkingStatusDownloaded: - if serverUrl == self.serverUrl || self.serverUrl.isEmpty { - await self.reloadDataSource() - } - case self.global.networkingStatusDownloadCancel: - if serverUrl == self.serverUrl { - await self.reloadDataSource() - } - // CREATE FOLDER - case self.global.networkingStatusCreateFolder: - if serverUrl == self.serverUrl, - selector != self.global.selectorUploadAutoUpload, - let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { - self.pushMetadata(metadata) - } - // RENAME - case self.global.networkingStatusRename: - if self.isSearchingMode { - self.networkSearch() - } else if self.serverUrl == serverUrl { - await self.reloadDataSource() - } - // FAVORITE - case self.global.networkingStatusFavorite: - if self.isSearchingMode { - self.networkSearch() - } else if self is NCFavorite { - await self.reloadDataSource() - } else if self.serverUrl == serverUrl { - await self.reloadDataSource() - } - default: - break - } - } - } - } - - func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) { - Task { - await self.debouncer.call { - if requestData { - if self.isSearchingMode { - self.networkSearch() - } else if ( self.serverUrl == serverUrl) || serverUrl == nil { - Task { - await self.getServerData() - } - } - } else { - if self.isSearchingMode { - guard status != self.global.metadataStatusWaitDelete, - status != self.global.metadataStatusWaitRename, - status != self.global.metadataStatusWaitMove, - status != self.global.metadataStatusWaitCopy, - status != self.global.metadataStatusWaitFavorite else { - return - } - self.networkSearch() - } else if ( self.serverUrl == serverUrl) || serverUrl == nil { - Task { - await self.reloadDataSource() - } - } - } - } - } - } - // MARK: - NotificationCenter @objc func applicationWillResignActive(_ notification: NSNotification) { @@ -931,12 +664,9 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return NCBrandOptions.shared.brand } - func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } - @MainActor func startGUIGetServerData() { self.dataSource.setGetServerData(false) - self.collectionView.reloadData() // Don't show spinner on iPad root folder if UIDevice.current.userInterfaceIdiom == .pad, @@ -966,61 +696,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.navigationItem.titleView = nil self.navigationItem.title = self.titleCurrentFolder } - - func resetPlusButtonAlpha(animated: Bool = true) { } - - func isHiddenPlusButton(_ isHidden: Bool) { } - - // MARK: - Empty - - func emptyDataSetView(_ view: NCEmptyView) { - - self.emptyDataSet?.setOffset(getHeaderHeight()) - if isSearchingMode { - view.emptyImage.image = UIImage(named: "search")?.image(color: .gray, size: UIScreen.main.bounds.width) - if self.dataSourceTask?.state == .running { - view.emptyTitle.text = NSLocalizedString("_search_in_progress_", comment: "") - } else { - view.emptyTitle.text = NSLocalizedString("_search_no_record_found_", comment: "") - } - view.emptyDescription.text = NSLocalizedString("_search_instruction_", comment: "") - } else if self.dataSourceTask?.state == .running { - view.emptyImage.image = UIImage(named: "networkInProgress")?.image(color: .gray, size: UIScreen.main.bounds.width) - view.emptyTitle.text = NSLocalizedString("_request_in_progress_", comment: "") - view.emptyDescription.text = "" - } else { - if serverUrl.isEmpty { - view.emptyImage.image = emptyImage - view.emptyTitle.text = NSLocalizedString(emptyTitle, comment: "") - view.emptyDescription.text = NSLocalizedString(emptyDescription, comment: "") - } else { - view.emptyImage.image = UIImage(named: "folder_nmcloud") - view.emptyTitle.text = NSLocalizedString("_files_no_files_", comment: "") - view.emptyDescription.text = NSLocalizedString("_no_file_pull_down_", comment: "") - } - } - - let spinner = UIActivityIndicatorView(style: .medium) - spinner.startAnimating() - - let container = UIView() - container.translatesAutoresizingMaskIntoConstraints = false - container.addSubview(spinner) - - spinner.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - spinner.centerXAnchor.constraint(equalTo: container.centerXAnchor), - spinner.centerYAnchor.constraint(equalTo: container.centerYAnchor) - ]) - - self.navigationItem.titleView = container - } - - @MainActor - func restoreDefaultTitle() { - self.navigationItem.titleView = nil - self.navigationItem.title = self.titleCurrentFolder - } // MARK: - SEARCH @@ -1031,7 +706,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS searchController?.searchBar.alpha = 1 } else { searchController?.searchBar.alpha = 0.3 - } } @@ -1062,6 +736,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.networking.cancelUnifiedSearchFiles() self.isSearchingMode = false + self.networkSearchInProgress = false self.literalSearch = "" self.providers?.removeAll() self.dataSource.removeAll() @@ -1084,11 +759,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) { guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } - - NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) -// NCDownloadAction.shared.openShare(viewController: self, metadata: metadata, page: .sharing) TealiumHelper.shared.trackEvent(title: "magentacloud-app.filebrowser.sharing", data: ["": ""]) appDelegate.adjust.trackEvent(TriggerEvent(Sharing.rawValue)) + + NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) } func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { @@ -1096,22 +770,26 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS toggleMenu(metadata: metadata, image: image, sender: sender) } - func tapRichWorkspace(_ sender: Any) { - if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { - if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { - viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" - viewerRichWorkspace.serverUrl = serverUrl - viewerRichWorkspace.delegate = self +// func tapRichWorkspace(_ sender: Any) { +// if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { +// if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { +// viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" +// viewerRichWorkspace.serverUrl = serverUrl +// viewerRichWorkspace.delegate = self +// +// navigationController.modalPresentationStyle = .fullScreen +// self.present(navigationController, animated: true, completion: nil) +// } +// } +// } - navigationController.modalPresentationStyle = .fullScreen - self.present(navigationController, animated: true, completion: nil) - } - } - } +// func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { +// toggleMenu(metadata: metadata, image: image, sender: sender) +// } - func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { - toggleMenu(metadata: metadata, image: image, sender: sender) - } +// func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { +// unifiedSearchMore(metadataForSection: metadataForSection) +// } func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { unifiedSearchMore(metadataForSection: metadataForSection) @@ -1330,52 +1008,21 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // MARK: - DataSource - @objc func reloadDataSource() { - - // get auto upload folder - autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() - autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: session.urlBase, userId: session.userId, account: session.account) - // get layout for view - layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) - // set GroupField for Grid - if !isSearchingMode && layoutForView?.layout == NCGlobal.shared.layoutGrid { - groupByField = "classFile" - } else { - groupByField = "name" - } - if isSearchingMode { - isDirectoryEncrypted = false - } else { - isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) - } - - DispatchQueue.main.async { -// UIView.transition(with: self.collectionView, -// duration: 0.20, -// options: .transitionCrossDissolve, -// animations: { self.collectionView.reloadData() }, -// completion: nil) -// -// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() - if isRecommendationActived { - Task.detached { + @MainActor + func reloadDataSource() async { + if !isSearchingMode { + Task.detached { + if await self.isRecommendationActived() { await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) } } } - DispatchQueue.main.async { -// UIView.transition(with: self.collectionView, -// duration: 0.20, -// options: .transitionCrossDissolve, -// animations: { self.collectionView.reloadData() }, -// completion: nil) -// -// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() - self.refreshControl.endRefreshing() - self.collectionView.reloadData() - self.setNavigationRightItems() + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: self.serverUrl) } + + await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() } func getServerData(forced: Bool = false) async { } @@ -1429,8 +1076,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } completion: { metadatasSearch, error in Task { guard let metadatasSearch, - error == .success, - self.isSearchingMode + error == .success, + self.isSearchingMode else { self.networkSearchInProgress = false await self.reloadDataSource() @@ -1457,7 +1104,11 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS guard let metadataForSection = metadataForSection, let lastSearchResult = metadataForSection.lastSearchResult, let cursor = lastSearchResult.cursor, let term = literalSearch else { return } metadataForSection.unifiedSearchInProgress = true - self.collectionView?.reloadData() + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: nil) + } + } self.networking.unifiedSearchFilesProvider(id: lastSearchResult.id, term: term, limit: 5, cursor: cursor, account: session.account) { task in self.searchDataSourceTask = task @@ -1466,12 +1117,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } completion: { _, searchResult, metadatas, error in if error != .success { - Task {@MainActor in - await showErrorBanner( - controller: self.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode - ) + Task { + await showErrorBanner(controller: self.controller, text: error.errorDescription) } } @@ -1479,8 +1126,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS guard let searchResult = searchResult, let metadatas = metadatas else { return } self.dataSource.appendMetadatasToSection(metadatas, metadataForSection: metadataForSection, lastSearchResult: searchResult) - DispatchQueue.main.async { - self.collectionView?.reloadData() + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: nil) + } } } } @@ -1513,21 +1162,38 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } - func pushViewController(viewController: UIViewController) { - if pushed { return } + // MARK: - Header size - pushed = true - navigationController?.pushViewController(viewController, animated: true) - } + func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, + heightHeaderRecommendations: CGFloat, + heightHeaderSection: CGFloat) { + var heightHeaderRichWorkspace: CGFloat = 0 + var heightHeaderRecommendations: CGFloat = 0 + var heightHeaderSection: CGFloat = 0 - // MARK: - Header size - - func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { - if headerMenuTransferView, - NCNetworking.shared.isOnline, - let results = database.getResultsMetadatas(predicate: NSPredicate(format: "status IN %@", [global.metadataStatusWaitUpload, global.metadataStatusUploading])), - !results.isEmpty { - return Array(results) + if showDescription, + !isSearchingMode, + let richWorkspaceText = self.richWorkspaceText, + !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { + heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 + } + + if isRecommendationActived, + !isSearchingMode, + NCPreferences().showRecommendedFiles, + !self.database.getRecommendedFiles(account: self.session.account).isEmpty { + heightHeaderRecommendations = self.heightHeaderRecommendations + heightHeaderSection = self.heightHeaderSection + } + + if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { + if section == 0 { + return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) + } else { + return (0, 0, self.heightHeaderSection) + } + } else { + return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) } } @@ -1606,6 +1272,107 @@ extension NCCollectionViewCommon: NCSectionFooterDelegate { } } +extension NCCollectionViewCommon: NCTransferDelegate { + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + + func transferReloadData(serverUrl: String?) { + Task { + await self.debouncerReloadData.call({ + self.collectionView.reloadData() + }, immediate: true) + } + } + + func transferChange(status: String, + account: String, + fileName: String, + serverUrl: String, + selector: String?, + ocId: String, + destination: String?, + error: NKError) { + Task { + if error != .success, + error.errorCode != global.errorResourceNotFound { + await showErrorBanner(controller: self.controller, text: error.errorDescription) + } + guard session.account == account else { + return + } + + if status == self.global.networkingStatusCreateFolder { + if serverUrl == self.serverUrl, + selector != self.global.selectorUploadAutoUpload, + let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { + self.pushMetadata(metadata) + } + return + } + + if self.isSearchingMode { + await self.debouncerNetworkSearch.call { + self.networkSearch() + } + } else if self.serverUrl == serverUrl || destination == self.serverUrl || self.serverUrl.isEmpty { + await self.debouncerReloadDataSource.call { + await self.reloadDataSource() + } + } + } + } + + func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { + Task { + if self.isSearchingMode { + await self.debouncerNetworkSearch.call { + self.networkSearch() + } + return + } + + if requestData && (self.serverUrl == serverUrl || serverUrl == nil) { + await self.debouncerGetServerData.call { + await self.getServerData() + } + return + } + + if self.serverUrl == serverUrl || serverUrl == nil { + await self.debouncerReloadDataSource.call { + await self.reloadDataSource() + } + } + } + } + + func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } +} + +extension NCCollectionViewCommon: NCSectionFirstHeaderDelegate { + func tapRichWorkspace(_ sender: Any) { + if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { + if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { + viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" + viewerRichWorkspace.serverUrl = serverUrl + viewerRichWorkspace.delegate = self + + navigationController.modalPresentationStyle = .fullScreen + self.present(navigationController, animated: true, completion: nil) + } + } + } + + func tapRecommendations(with metadata: tableMetadata) { + didSelectMetadata(metadata, withOcIds: false) + } +} + +extension NCCollectionViewCommon: NCSectionFooterDelegate { + func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { + unifiedSearchMore(metadataForSection: metadataForSection) + } +} + extension NCCollectionViewCommon: NCTransferDelegate { func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } diff --git a/iOSClient/NCBackgroundLocationUploadManager.swift b/iOSClient/NCBackgroundLocationUploadManager.swift index 8f63b82e9d..154fda662d 100644 --- a/iOSClient/NCBackgroundLocationUploadManager.swift +++ b/iOSClient/NCBackgroundLocationUploadManager.swift @@ -1,10 +1,6 @@ -// -// NCBackgroundLocationUploadManager.swift -// Nextcloud -// -// Created by Marino Faggiana on 06/06/25. -// Copyright © 2025 Marino Faggiana. All rights reserved. -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import CoreLocation import NextcloudKit @@ -106,15 +102,16 @@ class NCBackgroundLocationUploadManager: NSObject, CLLocationManagerDelegate { } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + // Must work only in background guard isAppInBackground else { return } // Open Realm - if database.openRealmBackground() { - let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! - let location = locations.last - nkLog(tag: self.global.logTagLocation, emoji: .start, message: "Triggered by location change: \(location?.coordinate.latitude ?? 0), \(location?.coordinate.longitude ?? 0)") + guard NCManageDatabase.shared.openRealmBackground() else { + nkLog(tag: self.global.logTagLocation, emoji: .error, message: "Failed to open Realm in Location Manager") + return + } let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! let location = locations.last diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index 3cb1bc1ec6..afa29afca6 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -50,7 +50,7 @@ final class NCGlobal: Sendable { // let avatarSize: Int = 128 * Int(UIScreen.main.scale) let avatarSizeRounded: Int = 128 - + // Preview size // let size1024: CGSize = CGSize(width: 1024, height: 1024) @@ -229,6 +229,7 @@ final class NCGlobal: Sendable { let selectorSynchronizationOffline = "synchronizationOffline" let selectorPrint = "print" let selectorDeleteFile = "deleteFile" + // Metadata : Status // // 0 normal @@ -258,6 +259,7 @@ final class NCGlobal: Sendable { let metadataStatusForScreenAwake = [-1, -2, 1, 2] let metadataStatusHideInView = [1, 2, 3, 11] let metadataStatusWaitWebDav = [10, 11, 12, 13, 14, 15] + let metadataStatusTransfers = [-2, -3, 2, 3, 10, 11, 12, 13, 14, 15] let metadatasStatusInWaiting = [-1, 1, 10, 11, 12, 13, 14, 15] let metadatasStatusInProgress = [-2, 2] @@ -274,8 +276,6 @@ final class NCGlobal: Sendable { let notificationCenterChangeTheming = "changeTheming" // userInfo: account let notificationCenterRichdocumentGrabFocus = "richdocumentGrabFocus" let notificationCenterReloadDataNCShare = "reloadDataNCShare" - let notificationCenterDidCreateShareLink = "didCreateShareLink" - let notificationCenterCloseRichWorkspaceWebView = "closeRichWorkspaceWebView" let notificationCenterReloadAvatar = "reloadAvatar" let notificationCenterClearCache = "clearCache" @@ -293,8 +293,6 @@ final class NCGlobal: Sendable { let notificationCenterFileExists = "fileExists" // userInfo: ocId, fileExists let notificationCenterReloadDataSource = "reloadDataSource" // userInfo: serverUrl?, clearDataSource - let notificationCenterRenameFile = "renameFile" // userInfo: serverUrl, account, error - let notificationCenterMenuSearchTextPDF = "menuSearchTextPDF" let notificationCenterMenuGotToPageInPDF = "menuGotToPageInPDF" @@ -426,7 +424,7 @@ final class NCGlobal: Sendable { let keyFileNameAutoUploadType = "fileNameAutoUploadType" let keyFileNameOriginal = "fileNameOriginal" let keyFileNameOriginalAutoUpload = "fileNameOriginalAutoUpload" - + // LOG TAG // let logTagTask = "BGT" diff --git a/iOSClient/Networking/NCConfigServer.swift b/iOSClient/Networking/NCConfigServer.swift index d5e4d2a867..f82b466647 100644 --- a/iOSClient/Networking/NCConfigServer.swift +++ b/iOSClient/Networking/NCConfigServer.swift @@ -1,25 +1,6 @@ -// -// NCConfigServer.swift -// Nextcloud -// -// Created by Marino Faggiana on 05/12/22. -// Copyright © 2022 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2022 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import UIKit diff --git a/iOSClient/Networking/NCNetworking+Download.swift b/iOSClient/Networking/NCNetworking+Download.swift index 22ecb91653..4d9fe0c51c 100644 --- a/iOSClient/Networking/NCNetworking+Download.swift +++ b/iOSClient/Networking/NCNetworking+Download.swift @@ -1,25 +1,6 @@ -// -// NCNetworking+Download.swift -// Nextcloud -// -// Created by Marino Faggiana on 07/02/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit import NextcloudKit @@ -346,40 +327,3 @@ extension NCNetworking { } } } - -class NCOperationDownload: ConcurrentOperation, @unchecked Sendable { - var metadata: tableMetadata - var selector: String - - init(metadata: tableMetadata, selector: String) { - self.metadata = tableMetadata.init(value: metadata) - self.selector = selector - } - - override func start() { - guard !isCancelled else { return self.finish() } - - metadata.session = NCNetworking.shared.sessionDownload - metadata.sessionError = "" - metadata.sessionSelector = selector - metadata.sessionTaskIdentifier = 0 - metadata.status = NCGlobal.shared.metadataStatusWaitDownload - -// let metadata = NCManageDatabase.shared.addMetadata(metadata) - -// NCNetworking.shared.download(metadata: metadata, withNotificationProgressTask: true) { -// } completion: { _, _ in -// self.finish() -// } - Task { - await download(withSelector: self.selector) - } - } - - private func download(withSelector selector: String = "") async { - await NCNetworking.shared.downloadFile(metadata: metadata) { _ in - self.finish() - } taskHandler: { _ in } - - } -} diff --git a/iOSClient/Networking/NCNetworking+LivePhoto.swift b/iOSClient/Networking/NCNetworking+LivePhoto.swift index 072b3b087f..e861b76b81 100644 --- a/iOSClient/Networking/NCNetworking+LivePhoto.swift +++ b/iOSClient/Networking/NCNetworking+LivePhoto.swift @@ -1,25 +1,6 @@ -// -// NCNetworking+LivePhoto.swift -// Nextcloud -// -// Created by Marino Faggiana on 07/02/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit import NextcloudKit diff --git a/iOSClient/Networking/NCNetworking+Task.swift b/iOSClient/Networking/NCNetworking+Task.swift index 1b6122252b..f28830b583 100644 --- a/iOSClient/Networking/NCNetworking+Task.swift +++ b/iOSClient/Networking/NCNetworking+Task.swift @@ -1,25 +1,6 @@ -// -// NCNetworking+Task.swift -// Nextcloud -// -// Created by Marino Faggiana on 24/08/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import UIKit diff --git a/iOSClient/Networking/NCNetworking+Upload.swift b/iOSClient/Networking/NCNetworking+Upload.swift index 6c985dbe7c..9467e9c0b7 100644 --- a/iOSClient/Networking/NCNetworking+Upload.swift +++ b/iOSClient/Networking/NCNetworking+Upload.swift @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + import UIKit import NextcloudKit import Alamofire @@ -41,22 +45,6 @@ extension NCNetworking { } taskHandler(task) } progressHandler: { progress in - Task { - guard await self.progressQuantizer.shouldEmit(serverUrlFileName: serverUrlFileName, fraction: progress.fractionCompleted) else { - return - } - - if let metadata { - await NCManageDatabase.shared.setMetadataProgress(ocId: metadata.ocId, progress: progress.fractionCompleted) - await self.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferProgressDidUpdate(progress: Float(progress.fractionCompleted), - totalBytes: progress.totalUnitCount, - totalBytesExpected: progress.completedUnitCount, - fileName: metadata.fileName, - serverUrl: metadata.serverUrl) - } - } - } progressHandler(progress.completedUnitCount, progress.totalUnitCount, progress.fractionCompleted) } @@ -390,7 +378,6 @@ extension NCNetworking { error: self.global.diagnosticProblemsUploadServerError) } } - await NCManageDatabase.shared.updateBadge() } // MARK: - diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 4d78a1844e..37629dadd1 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -1,25 +1,6 @@ -// -// NCNetworking+WebDAV.swift -// Nextcloud -// -// Created by Marino Faggiana on 07/02/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit import NextcloudKit @@ -484,50 +465,6 @@ extension NCNetworking { return .success } - - func renameMetadata(_ metadata: tableMetadata, - fileNameNew: String, - indexPath: IndexPath, - viewController: UIViewController?, - completion: @escaping (_ error: NKError) -> Void) { - - let permission = NCMetadataPermissions.permissionsContainsString(metadata.permissions, permissions: NCMetadataPermissions.permissionCanRename) - if (!metadata.permissions.isEmpty && permission == false) || - (metadata.status != global.metadataStatusNormal && metadata.status != global.metadataStatusWaitRename) { -// return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_no_permission_modify_file_")) -// let error = await NCNetworkingE2EERename().rename(metadata: metadata, fileNameNew: fileNameNew) -// DispatchQueue.main.async { completion(error) } - completion(NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_no_permission_modify_file_")) - } - - if metadata.isDirectoryE2EE { -#if !EXTENSION - if isOffline { -// return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) - completion(NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) - } - Task { - let error = await NCNetworkingE2EERename().rename(metadata: metadata, fileNameNew: fileNameNew) - if error != .success { -// NCContentPresenter().showError(error: error) - completion(error) - } - } -#endif - } else { - Task { - await self.transferDispatcher.notifyAllDelegatesAsync { delegate in - let status = self.global.metadataStatusWaitRename - await NCManageDatabase.shared.renameMetadata(fileNameNew: fileNameNew, ocId: metadata.ocId, status: status) - delegate.transferReloadData(serverUrl: metadata.serverUrl, status: status) - } - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterRenameFile, userInfo: ["serverUrl": metadata.serverUrl, "account": metadata.account, "error": NKError(errorCode: 0, errorDescription: ""), "ocId": metadata.ocId, "indexPath": indexPath]) - - } - - completion(NKError(errorCode: 0, errorDescription: "")) - } - } func renameFileOrFolder(metadata: tableMetadata) async -> NKError { let serverUrlFileNameSource = metadata.serverUrlFileName diff --git a/iOSClient/Networking/NCNetworking.swift b/iOSClient/Networking/NCNetworking.swift index 0c86cf84c8..c0684756d4 100644 --- a/iOSClient/Networking/NCNetworking.swift +++ b/iOSClient/Networking/NCNetworking.swift @@ -37,203 +37,6 @@ protocol NCTransferDelegate: AnyObject { serverUrl: String) } -extension NCTransferDelegate { - func transferChange(status: String, - account: String, - fileName: String, - serverUrl: String, - selector: String?, - ocId: String, - destination: String?, - error: NKError) {} - func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) {} - func transferProgressDidUpdate(progress: Float, - totalBytes: Int64, - totalBytesExpected: Int64, - fileName: String, - serverUrl: String) {} -} - -/// Actor-based delegate dispatcher using weak references. -actor NCTransferDelegateDispatcher { - // Weak reference collection of delegates - private var transferDelegates = NSHashTable.weakObjects() - - /// Adds a delegate safely. - func addDelegate(_ delegate: NCTransferDelegate) { - transferDelegates.add(delegate) - } - - /// Remove a delegate safely. - func removeDelegate(_ delegate: NCTransferDelegate) { - transferDelegates.remove(delegate) - } - - /// Notifies all delegates. - func notifyAllDelegates(_ block: (NCTransferDelegate) -> Void) { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - block(delegate) - } - } - - func notifyAllDelegatesAsync(_ block: @escaping (NCTransferDelegate) async -> Void) async { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - await block(delegate) - } - } - - /// Notifies the delegate for a specific scene. - func notifyDelegate(forScene sceneIdentifier: String, _ block: (NCTransferDelegate) -> Void) { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - if delegate.sceneIdentifier == sceneIdentifier { - block(delegate) - } - } - } - - /// Notifies matching and non-matching delegates for a specific scene. - func notifyDelegates(forScene sceneIdentifier: String, - matching: (NCTransferDelegate) -> Void, - others: (NCTransferDelegate) -> Void) { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - if delegate.sceneIdentifier == sceneIdentifier { - matching(delegate) - } else { - others(delegate) - } - } - } -} - -/// A thread-safe registry for tracking in-flight `URLSessionTask` instances. -/// -/// Each task is associated with a string identifier (`identifier`) that you define, -/// allowing you to check whether a request is already running, avoid duplicates, -/// and cancel all active tasks at once. The registry automatically removes -/// completed tasks via `cleanupCompleted()` to keep memory usage compact. -/// -/// Typical use cases: -/// - Ensure only one task per identifier is active at a time. -/// - Query whether a specific request is still running (`isReading`). -/// - Forcefully stop a specific request (`cancel`). -/// - Forcefully stop all tasks when leaving a screen (`cancelAll`). -actor NetworkingTasks { - private var active: [(identifier: String, task: URLSessionTask)] = [] - - /// Returns whether there is an in-flight task for the given URL. - /// - /// A task is considered in-flight if its `state` is `.running` or `.suspended`. - /// - Parameter identifier: The identifier to check. - /// - Returns: `true` if a matching in-flight task exists; otherwise `false`. - func isReading(identifier: String) -> Bool { - // Drop finished/canceling tasks globally - cleanup() - - return active.contains { - $0.identifier == identifier && ($0.task.state == .running || $0.task.state == .suspended) - } - } - - /// Tracks a newly created `URLSessionTask` for the given identifier. - /// - /// If a running entry for the same identifier exists, it is removed before appending the new one. - /// - Parameters: - /// - identifier: The identifier associated with the task. - /// - task: The `URLSessionTask` to track. - func track(identifier: String, task: URLSessionTask) { - // Drop finished/canceling tasks globally - cleanup() - - active.removeAll { - $0.identifier == identifier && $0.task.state == .running - } - active.append((identifier, task)) - nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .start, message: "Start task for identifier: \(identifier)", consoleOnly: true) - } - - /// create a Identifier - /// - func createIdentifier(account: String? = nil, path: String? = nil, name: String) -> String { - if let account, - let path { - return account + "_" + path + "_" + name - } else if let path { - return path + "_" + name - } else { - return name - } - } - - /// Cancels and removes all tasks associated with the given id. - /// - /// - Parameter identifier: The identifier whose tasks should be canceled. - func cancel(identifier: String) { - // Drop finished/canceling tasks globally - cleanup() - - for element in active where element.identifier == identifier { - element.task.cancel() - nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .cancel, message: "Cancel task for identifier: \(identifier)", consoleOnly: true) - } - active.removeAll { - $0.identifier == identifier - } - } - - /// Cancels all tracked `URLSessionTask` and clears the registry. - /// - /// Call this when leaving the page/screen or when the operation must be forcefully stopped. - func cancelAll() { - active.forEach { - $0.task.cancel() - nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .cancel, message: "Cancel task with identifier: \($0.identifier)", consoleOnly: true) - } - active.removeAll() - } - - /// Removes tasks that have completed from the registry. - /// - /// Useful to keep the in-memory list compact during long-running operations. - func cleanup() { - active.removeAll { - $0.task.state == .completed || $0.task.state == .canceling - } - } -} - -/// Quantizes per-task progress updates to integer percentages (0...100). -/// Each (serverUrlFileName) pair is tracked separately, so you get -/// at most one update per integer percent for each transfer. -actor ProgressQuantizer { - private var lastPercent: [String: Int] = [:] - - /// Returns `true` only when integer percent changes (or hits 100). - /// - /// - Parameters: - /// - serverUrlFileName: The name of the file being transferred. - /// - fraction: Progress fraction [0.0 ... 1.0]. - func shouldEmit(serverUrlFileName: String, fraction: Double) -> Bool { - let percent = min(max(Int((fraction * 100).rounded(.down)), 0), 100) - - let last = lastPercent[serverUrlFileName] ?? -1 - guard percent != last || percent == 100 else { - return false - } - - lastPercent[serverUrlFileName] = percent - return true - } - - /// Clears stored state for a finished transfer. - func clear(serverUrlFileName: String) { - lastPercent.removeValue(forKey: serverUrlFileName) - } -} - class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { static let shared = NCNetworking() @@ -291,7 +94,6 @@ class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { let downloadAvatarQueue = Queuer(name: "downloadAvatarQueue", maxConcurrentOperationCount: 10, qualityOfService: .default) let fileExistsQueue = Queuer(name: "fileExistsQueue", maxConcurrentOperationCount: 10, qualityOfService: .default) #endif - let downloadQueue = Queuer(name: "downloadQueue", maxConcurrentOperationCount: NCBrandOptions.shared.httpMaximumConnectionsPerHostInDownload, qualityOfService: .default) // MARK: - init diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index 9121911b0b..930a965320 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -271,6 +271,7 @@ actor NCNetworkingProcess { await startTimer(interval: minInterval) } } else { + // Remove upload asset await removeUploadedAssetsIfNeeded() // Set Live Photo diff --git a/iOSClient/Networking/NCService.swift b/iOSClient/Networking/NCService.swift index 2987e2543c..f6715693d3 100644 --- a/iOSClient/Networking/NCService.swift +++ b/iOSClient/Networking/NCService.swift @@ -1,25 +1,6 @@ -// -// NCService.swift -// Nextcloud -// -// Created by Marino Faggiana on 14/03/18. -// Copyright © 2018 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2018 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit @preconcurrency import NextcloudKit diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index e4497c92bc..782f3e76bb 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -13,7 +13,7 @@ import XLForm class PrivacySettingsViewController: XLFormViewController{ - @objc public var isShowSettingsButton: Bool = false + @objc public var isShowSettingsButton: Bool = false override func viewDidLoad() { super.viewDidLoad() @@ -36,6 +36,8 @@ class PrivacySettingsViewController: XLFormViewController{ initializeForm() } + + //MARK: XLForm func initializeForm() { @@ -56,6 +58,7 @@ class PrivacySettingsViewController: XLFormViewController{ section.footerTitle = NSLocalizedString("_privacy_settings_help_text_", comment: "") form.addFormSection(section) + //custom cell section = XLFormSectionDescriptor.formSection(withTitle: "") section.footerTitle = NSLocalizedString("_required_data_collection_help_text_", comment: "") @@ -64,6 +67,7 @@ class PrivacySettingsViewController: XLFormViewController{ XLFormViewController.cellClassesForRowDescriptorTypes()["RequiredDataCollectionCustomCellType"] = RequiredDataCollectionSwitch.self + row = XLFormRowDescriptor(tag: "ButtonDestinationFolder", rowType: "RequiredDataCollectionCustomCellType", title: "") row.cellConfig["requiredDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue @@ -78,6 +82,7 @@ class PrivacySettingsViewController: XLFormViewController{ XLFormViewController.cellClassesForRowDescriptorTypes()["AnalysisDataCollectionCustomCellType"] = AnalysisDataCollectionSwitch.self + row = XLFormRowDescriptor(tag: "AnalysisDataCollectionSwitch", rowType: "AnalysisDataCollectionCustomCellType", title: "") row.cellConfig["analysisDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue @@ -92,11 +97,13 @@ class PrivacySettingsViewController: XLFormViewController{ section.addFormRow(row) + XLFormViewController.cellClassesForRowDescriptorTypes()["SaveSettingsButton"] = SaveSettingsCustomButtonCell.self section = XLFormSectionDescriptor.formSection(withTitle: "") form.addFormSection(section) + row = XLFormRowDescriptor(tag: "SaveSettingsButton", rowType: "SaveSettingsButton", title: "") row.cellConfig["backgroundColor"] = UIColor.clear diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 989135a5fe..7d20929728 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -232,6 +232,8 @@ "_settings_account_request_" = "Request account at startup"; "_print_" = "Print"; "_alias_" = "Alias"; +//"_alias_placeholder_" = "Write alias"; +//"_alias_footer_" = "Give your accounts a descriptive name such as Home, Office, School…"; "_alias_placeholder_" = "Write the alias"; "_alias_footer_" = "Give your account names a descriptive name such as Home, Office, School …"; "_chunk_size_mb_" = "Chunk size in MB"; @@ -739,6 +741,7 @@ "_share_link_" = "Share link"; //"_share_link_" = "Share link"; "_share_link_" = "Link"; +//"_share_link_" = "Share link"; "_share_link_button_" = "Send link to …"; "_share_link_name_" = "Link name"; "_password_" = "Password"; From 99fd8cfa783aa5ec0be1d61dedf8d8e4281b7962 Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:58:19 +0530 Subject: [PATCH 32/40] NMC 1984 - Privacy policy customisation added --- .../NCCollectionViewCommon.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 8cb4844927..071840d113 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -239,6 +239,14 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let dropInteraction = UIDropInteraction(delegate: self) self.navigationController?.navigationItem.leftBarButtonItems?.first?.customView?.addInteraction(dropInteraction) + + if(!UserDefaults.standard.bool(forKey: "isInitialPrivacySettingsShowed") || isApplicationUpdated()){ + redirectToPrivacyViewController() + + //set current app version + let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String + UserDefaults.standard.set(appVersion, forKey: "CurrentAppVersion") + } if(!UserDefaults.standard.bool(forKey: "isInitialPrivacySettingsShowed") || isApplicationUpdated()){ redirectToPrivacyViewController() @@ -375,6 +383,19 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, removeImageCache(metadatas: self.dataSource.getMetadatas()) } + + func isApplicationUpdated() -> Bool{ + let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" + let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") + return currentVersion != appVersion + } + + func redirectToPrivacyViewController(){ + let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) + let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController + newViewController.modalPresentationStyle = .fullScreen + self.present(newViewController, animated: true, completion: nil) + } func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { let viewController = presentationController.presentedViewController From a1d89e2dcef26c9fe6f5f53118e2d888acfc598b Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Wed, 9 Apr 2025 14:10:39 +0530 Subject: [PATCH 33/40] NMC 1984 - Privacy policy customisation changes added --- ...mon+CollectionViewDelegateFlowLayout.swift | 43 + .../NCCollectionViewCommon.swift | 1367 +++++++++++------ 2 files changed, 935 insertions(+), 475 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift index 06795639bc..42bbbd6038 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift @@ -13,4 +13,47 @@ extension NCCollectionViewCommon: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { return sizeForFooterInSection(section: section) } + + func getHeaderHeight() -> CGFloat { + + var size: CGFloat = 0 + // transfer in progress + if headerMenuTransferView, + let metadata = NCManageDatabase.shared.getMetadataFromOcId(NCNetworking.shared.transferInForegorund?.ocId), + metadata.isTransferInForeground { + if !isSearchingMode { + size += NCGlobal.shared.heightHeaderTransfer + } + } else { + NCNetworking.shared.transferInForegorund = nil + } + + if headerMenuButtonsView { + size += NCGlobal.shared.heightButtonsView + } + + return size + } + + func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { + + var headerRichWorkspace: CGFloat = 0 + + if let richWorkspaceText = richWorkspaceText, showDescription { + let trimmed = richWorkspaceText.trimmingCharacters(in: .whitespaces) + if !trimmed.isEmpty && !isSearchingMode { + headerRichWorkspace = UIScreen.main.bounds.size.height / 6 + } + } + + if isSearchingMode || layoutForView?.layout == NCGlobal.shared.layoutGrid || dataSource.numberOfSections() > 1 { + if section == 0 { + return (getHeaderHeight(), headerRichWorkspace, NCGlobal.shared.heightSection) + } else { + return (0, 0, NCGlobal.shared.heightSection) + } + } else { + return (getHeaderHeight(), headerRichWorkspace, 0) + } + } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 071840d113..d7e593d66f 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -10,18 +10,20 @@ import EasyTipView import LucidBanner class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { +//class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionHeaderMenuDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate, NCEmptyDataSetDelegate { @IBOutlet weak var collectionView: UICollectionView! - let database = NCManageDatabase.shared - let global = NCGlobal.shared - let utility = NCUtility() - let utilityFileSystem = NCUtilityFileSystem() - let imageCache = NCImageCache.shared - var dataSource = NCCollectionViewDataSource() - let networking = NCNetworking.shared - let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! - var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() + internal let database = NCManageDatabase.shared + internal let global = NCGlobal.shared + internal let utility = NCUtility() + internal let utilityFileSystem = NCUtilityFileSystem() + internal let imageCache = NCImageCache.shared + internal var dataSource = NCCollectionViewDataSource() + internal let networking = NCNetworking.shared + internal let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! + internal var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() + private var isNavigatingMetadata = false var autoUploadFileName = "" var autoUploadDirectory = "" @@ -68,75 +70,84 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, var headerRichWorkspaceDisable: Bool = false var groupByField = "name" - var headerRichWorkspaceDisable: Bool = false - var emptyImageName: String? - var emptyImageColors: [UIColor]? - var emptyTitle: String = "" + var emptyImage: UIImage? - var emptyDescription: String = "" - var emptyDataPortaitOffset: CGFloat = 0 - var emptyDataLandscapeOffset: CGFloat = -20 + internal var emptyDescription: String = "" + internal var emptyDataPortaitOffset: CGFloat = 0 + internal var emptyDataLandscapeOffset: CGFloat = -20 - var lastScale: CGFloat = 1.0 - var currentScale: CGFloat = 1.0 - var maxColumns: Int { + internal var lastScale: CGFloat = 1.0 + internal var currentScale: CGFloat = 1.0 + internal var maxColumns: Int { let screenWidth = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) let column = Int(screenWidth / 44) return column } - var transitionColumns = false - var numberOfColumns: Int = 0 - var lastNumberOfColumns: Int = 0 + internal var transitionColumns = false + internal var numberOfColumns: Int = 0 + internal var lastNumberOfColumns: Int = 0 + var isTransitioning: Bool = false + var selectableDataSource: [RealmSwiftObject] { dataSource.getMetadataSourceForAllSections() } + var pushed: Bool = false + var emptyDataSet: NCEmptyDataSet? + let heightHeaderRecommendations: CGFloat = 160 let heightHeaderSection: CGFloat = 30 - @MainActor - var session: NCSession.Session { - NCSession.shared.getSession(controller: tabBarController) - } - - var isLayoutPhoto: Bool { + internal var isLayoutPhoto: Bool { layoutForView?.layout == global.layoutPhotoRatio || layoutForView?.layout == global.layoutPhotoSquare } - var isLayoutGrid: Bool { + internal var isLayoutGrid: Bool { layoutForView?.layout == global.layoutGrid } - var isLayoutList: Bool { + internal var isLayoutList: Bool { layoutForView?.layout == global.layoutList } - var showDescription: Bool { + internal var showDescription: Bool { !headerRichWorkspaceDisable && NCPreferences().showDescription } - var isRecommendationActived: Bool { + internal var isRecommendationActived: Bool { let capabilities = NCNetworking.shared.capabilities[session.account] ?? NKCapabilities.Capabilities() return self.serverUrl == self.utilityFileSystem.getHomeServer(session: self.session) && capabilities.recommendations } - var infoLabelsSeparator: String { + internal var infoLabelsSeparator: String { layoutForView?.layout == global.layoutList ? " - " : "" } @MainActor - var controller: NCMainTabBarController? { + internal var session: NCSession.Session { + NCSession.shared.getSession(controller: tabBarController) + } + + @MainActor + internal var controller: NCMainTabBarController? { self.tabBarController as? NCMainTabBarController } - var mainNavigationController: NCMainNavigationController? { + @MainActor + internal var mainNavigationController: NCMainNavigationController? { self.navigationController as? NCMainNavigationController } - var sceneIdentifier: String { + @MainActor + internal var sceneIdentifier: String { (self.tabBarController as? NCMainTabBarController)?.sceneIdentifier ?? "" } - var isNumberOfItemsInAllSectionsNull: Bool { + @MainActor + internal var windowScene: UIWindowScene? { + SceneManager.shared.getWindowScene(controller: self.tabBarController as? NCMainTabBarController) + } + + internal var isNumberOfItemsInAllSectionsNull: Bool { var totalItems = 0 for section in 0.. Bool{ + func isApplicationUpdated() -> Bool { let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") return currentVersion != appVersion } - func redirectToPrivacyViewController(){ + func redirectToPrivacyViewController() { let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController newViewController.modalPresentationStyle = .fullScreen self.present(newViewController, animated: true, completion: nil) } + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate(alongsideTransition: { _ in + self.collectionView?.collectionViewLayout.invalidateLayout() + }) + + self.dismissTip() + } + + override var canBecomeFirstResponder: Bool { + return true + } + func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { let viewController = presentationController.presentedViewController @@ -627,137 +703,516 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 - func changeLayout(withSubFolders: Bool) { - if self.layoutForView?.layout == layoutForView.layout { - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) - Task { - await self.reloadDataSource() - } - return - } - - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) - layoutForView.layout = layoutForView.layout - self.layoutType = layoutForView.layout - - collectionView.reloadData() - - switch layoutForView.layout { - case global.layoutList: - self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) - case global.layoutGrid: - self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) - case global.layoutPhotoSquare, global.layoutPhotoRatio: - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) - default: - break - } - - self.collectionView.collectionViewLayout.invalidateLayout() - + if serverUrl == homeServer || numFoldersLayoutsForView == 1 { Task { - await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + await setLayout(layoutForView: layoutForView) + await self.reloadDataSource() } - } - - if serverUrl == homeServer || numFoldersLayoutsForView == 1 { - changeLayout(withSubFolders: false) } else { let alertController = UIAlertController(title: NSLocalizedString("_propagate_layout_", comment: ""), message: nil, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default, handler: { _ in - changeLayout(withSubFolders: true) + Task { + await self.setLayout(layoutForView: layoutForView, withSubFolders: true) + await self.reloadDataSource() + } })) alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in - changeLayout(withSubFolders: false) + Task { + await self.setLayout(layoutForView: layoutForView) + await self.reloadDataSource() + } })) self.present(alertController, animated: true) } + self.collectionView.collectionViewLayout.invalidateLayout() + +// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() } - func getNavigationTitle() -> String { - let tblAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) - if let tblAccount, - !tblAccount.alias.isEmpty { - return tblAccount.alias + internal func setLayout(layoutForView: NCDBLayoutForView, withSubFolders: Bool = false) async { + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + layoutForView.layout = layoutForView.layout + self.layoutType = layoutForView.layout + + collectionView.reloadData() + + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break } - return NCBrandOptions.shared.brand + + collectionView.collectionViewLayout.invalidateLayout() } - @MainActor - func startGUIGetServerData() { - self.dataSource.setGetServerData(false) + @objc func reloadDataSource(_ notification: NSNotification) { + if let userInfo = notification.userInfo as? NSDictionary { + if let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } + } - // Don't show spinner on iPad root folder - if UIDevice.current.userInterfaceIdiom == .pad, - (self.serverUrl == self.utilityFileSystem.getHomeServer(session: self.session)) || self.serverUrl.isEmpty { - return + if let clearDataSource = userInfo["clearDataSource"] as? Bool, clearDataSource { + self.dataSource.removeAll() + } } - let spinner = UIActivityIndicatorView(style: .medium) - spinner.startAnimating() + reloadDataSource() + } - let container = UIView() - container.translatesAutoresizingMaskIntoConstraints = false - container.addSubview(spinner) + @objc func getServerData(_ notification: NSNotification) { + if let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } + } - spinner.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - spinner.centerXAnchor.constraint(equalTo: container.centerXAnchor), - spinner.centerYAnchor.constraint(equalTo: container.centerYAnchor) - ]) + getServerData() + } - self.navigationItem.titleView = container + @objc func reloadHeader(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + account == session.account + else { return } + + self.collectionView.reloadData() } - @MainActor - func stopGUIGetServerData() { - self.dataSource.setGetServerData(true) - self.navigationItem.titleView = nil - self.navigationItem.title = self.titleCurrentFolder + @objc func changeStatusFolderE2EE(_ notification: NSNotification) { + reloadDataSource() } - // MARK: - SEARCH + @objc func closeRichWorkspaceWebView() { + reloadDataSource() + } - func searchController(enabled: Bool) { - guard enableSearchBar else { return } - searchController?.searchBar.isUserInteractionEnabled = enabled - if enabled { - searchController?.searchBar.alpha = 1 + @objc func deleteFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError else { return } + + if error == .success { + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } } else { - searchController?.searchBar.alpha = 0.3 + NCContentPresenter().showError(error: error) } - } - func updateSearchResults(for searchController: UISearchController) { - self.literalSearch = searchController.searchBar.text + reloadDataSource() } - func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { - isSearchingMode = true - self.providers?.removeAll() - self.dataSource.removeAll() - Task { - await self.reloadDataSource() + @objc func copyMoveFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + account == session.account else { return } + + if isSearchingMode { + return networkSearch() } - // TIP - dismissTip() - // - mainNavigationController?.hiddenPlusButton(true) - } - func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { - if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { - networkSearch() + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + + if serverUrl == self.serverUrl { + reloadDataSource() } } - func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - self.networking.cancelUnifiedSearchFiles() + @objc func renameFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let error = userInfo["error"] as? NKError, + account == session.account + else { return } - self.isSearchingMode = false - self.networkSearchInProgress = false + if error == .success { + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + } + + if serverUrl == self.serverUrl { + if error != .success { + NCContentPresenter().showError(error: error) + } + reloadDataSource() + } else { + collectionView.reloadData() + } + } + + @objc func createFolder(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let account = userInfo["account"] as? String, + account == session.account, + let withPush = userInfo["withPush"] as? Bool, + let metadata = database.getMetadataFromOcId(ocId) + else { return } + + if isSearchingMode { + return networkSearch() + } + + if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { + reloadDataSource() + } else if withPush, metadata.serverUrl == self.serverUrl { + reloadDataSource() + if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { + if sceneIdentifier == controller?.sceneIdentifier { + pushMetadata(metadata) + } + } else { + pushMetadata(metadata) + } + } + } + + @objc func favoriteFile(_ notification: NSNotification) { + if isSearchingMode { + return networkSearch() + } + + if self is NCFavorite { + return reloadDataSource() + } + + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + serverUrl == self.serverUrl + else { return } + + reloadDataSource() + } + + @objc func downloadStartFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func downloadedFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func downloadCancelFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadStartFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + !isSearchingMode, + let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) + else { return } + + // Header view trasfer + if metadata.isTransferInForeground { + NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) + DispatchQueue.main.async { self.collectionView?.reloadData() } + } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadedFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadedLivePhoto(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadCancelFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func updateShare(_ notification: NSNotification) { + if isSearchingMode { + networkSearch() + } else { + self.dataSource.removeAll() + getServerData() + } + } + + @objc func triggerProgressTask(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let progressNumber = userInfo["progress"] as? NSNumber, + let totalBytes = userInfo["totalBytes"] as? Int64, + let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, + let ocId = userInfo["ocId"] as? String, + let ocIdTransfer = userInfo["ocIdTransfer"] as? String, + let session = userInfo["session"] as? String + else { return } + + let chunk: Int = userInfo["chunk"] as? Int ?? 0 + let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false + + let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) + + // HEADER +// if self.headerMenuTransferView, transfer.session.contains("upload") { +// self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) +// self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) +// } + + DispatchQueue.main.async { + if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { + if NCNetworking.shared.transferInForegorund?.ocId == ocId { + NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue + } else { + NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) + self.collectionView.reloadData() + } + self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue + } else { + guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, + let cell = self.collectionView?.cellForItem(at: indexPath), + let cell = cell as? NCCellProtocol else { return } + if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { + cell.fileProgressView?.isHidden = true + cell.fileProgressView?.progress = .zero + cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) + if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { + cell.writeInfoDateSize(date: metadata.date, size: metadata.size) + } else { + cell.fileInfoLabel?.text = "" + cell.fileSubinfoLabel?.text = "" + } + } else { + cell.fileProgressView?.isHidden = false + cell.fileProgressView?.progress = progressNumber.floatValue + cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) + let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal + if status == NCGlobal.shared.metadataStatusDownloading { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) + } else if status == NCGlobal.shared.metadataStatusUploading { + if totalBytes > 0 { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) + } else { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" + } + } + } + } + } + } + + // MARK: - Layout + + func setNavigationLeftItems() { + navigationItem.title = titleCurrentFolder + } + + func getNavigationTitle() -> String { + let tblAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) + if let tblAccount, + !tblAccount.alias.isEmpty { + return tblAccount.alias + } + return NCBrandOptions.shared.brand + } + + @MainActor + func startGUIGetServerData() { + self.dataSource.setGetServerData(false) + + // Don't show spinner on iPad root folder + if UIDevice.current.userInterfaceIdiom == .pad, + (self.serverUrl == self.utilityFileSystem.getHomeServer(session: self.session)) || self.serverUrl.isEmpty { + return + } + + let spinner = UIActivityIndicatorView(style: .medium) + spinner.startAnimating() + + let container = UIView() + container.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(spinner) + + spinner.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + spinner.centerXAnchor.constraint(equalTo: container.centerXAnchor), + spinner.centerYAnchor.constraint(equalTo: container.centerYAnchor) + ]) + + self.navigationItem.titleView = container + } + + @MainActor + func stopGUIGetServerData() { + self.dataSource.setGetServerData(true) + self.navigationItem.titleView = nil + self.navigationItem.title = self.titleCurrentFolder + } + + // MARK: - Empty + + func emptyDataSetView(_ view: NCEmptyView) { + + self.emptyDataSet?.setOffset(getHeaderHeight()) + if isSearchingMode { + view.emptyImage.image = UIImage(named: "search")?.image(color: .gray, size: UIScreen.main.bounds.width) + if self.dataSourceTask?.state == .running { + view.emptyTitle.text = NSLocalizedString("_search_in_progress_", comment: "") + } else { + view.emptyTitle.text = NSLocalizedString("_search_no_record_found_", comment: "") + } + view.emptyDescription.text = NSLocalizedString("_search_instruction_", comment: "") + } else if self.dataSourceTask?.state == .running { + view.emptyImage.image = UIImage(named: "networkInProgress")?.image(color: .gray, size: UIScreen.main.bounds.width) + view.emptyTitle.text = NSLocalizedString("_request_in_progress_", comment: "") + view.emptyDescription.text = "" + } else { + if serverUrl.isEmpty { + view.emptyImage.image = emptyImage + view.emptyTitle.text = NSLocalizedString(emptyTitle, comment: "") + view.emptyDescription.text = NSLocalizedString(emptyDescription, comment: "") + } else { + view.emptyImage.image = UIImage(named: "folder_nmcloud") + view.emptyTitle.text = NSLocalizedString("_files_no_files_", comment: "") + view.emptyDescription.text = NSLocalizedString("_no_file_pull_down_", comment: "") + } + } + } + + // MARK: - SEARCH + + func searchController(enabled: Bool) { + guard enableSearchBar else { + return + } + searchController?.searchBar.isUserInteractionEnabled = enabled + + if enabled { + searchController?.searchBar.alpha = 1 + } else { + searchController?.searchBar.alpha = 0.3 + } + } + + func updateSearchResults(for searchController: UISearchController) { + searchResultText = searchController.searchBar.text + } + + func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + // TIP + dismissTip() + + // (+) + self.mainNavigationController?.menuPlus?.hiddenPlusButton(true) + + if !isSearchingMode { + self.isSearchingMode = true + self.dataSource.removeAll() + self.collectionView.reloadData() + } + } + + func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { + if isSearchingMode, + searchResultText?.count ?? 0 >= 2 { + Task { + await self.search() + } +// if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { +// networkSearch() + } + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + self.networking.cancelUnifiedSearchFiles() + + self.isSearchingMode = false self.literalSearch = "" self.providers?.removeAll() self.dataSource.removeAll() @@ -770,37 +1225,118 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // MARK: - TAP EVENT - func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { - tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) + func tapMoreListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) } - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { - tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) } func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) { + if isEditMode { return } guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } - TealiumHelper.shared.trackEvent(title: "magentacloud-app.filebrowser.sharing", data: ["": ""]) - appDelegate.adjust.trackEvent(TriggerEvent(Sharing.rawValue)) NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) +// NCDownloadAction.shared.openShare(viewController: self, metadata: metadata, page: .sharing) + TealiumHelper.shared.trackEvent(title: "magentacloud-app.filebrowser.sharing", data: ["": ""]) + appDelegate.adjust.trackEvent(TriggerEvent(Sharing.rawValue)) } - func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + func tapMoreGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { + if isEditMode { return } guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } - toggleMenu(metadata: metadata, image: image, sender: sender) +// toggleMenu(metadata: metadata, image: image) + if namedButtonMore == NCGlobal.shared.buttonMoreMore || namedButtonMore == NCGlobal.shared.buttonMoreLock { + toggleMenu(metadata: metadata, image: image) + } else if namedButtonMore == NCGlobal.shared.buttonMoreStop { + Task { + await cancelSession(metadata: metadata) + } + } } -// func tapRichWorkspace(_ sender: Any) { -// if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { -// if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { -// viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" -// viewerRichWorkspace.serverUrl = serverUrl -// viewerRichWorkspace.delegate = self -// -// navigationController.modalPresentationStyle = .fullScreen -// self.present(navigationController, animated: true, completion: nil) -// } + func tapRichWorkspace(_ sender: Any) { + if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { + if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { + viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" + viewerRichWorkspace.serverUrl = serverUrl + viewerRichWorkspace.delegate = self + + navigationController.modalPresentationStyle = .fullScreen + self.present(navigationController, animated: true, completion: nil) + } + } + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + // (+) + self.mainNavigationController?.menuPlus?.hiddenPlusButton(false) + + self.isSearchingMode = false + self.networkSearchInProgress = false + self.searchResultText = nil + self.searchResultStore = nil + + Task { + await searchOperationHandle.cancel() + await reloadDataSource() + + // Restore Layout + if let layoutForViewLayoutStore { + let layoutForView = database.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) + layoutForView.layout = layoutForViewLayoutStore + await setLayout(layoutForView: layoutForView) + } + layoutForViewLayoutStore = nil + + // update Option menu + await mainNavigationController?.updateMenuOption() + } + } + + @MainActor + func setSearchBarLoading(_ loading: Bool) { + guard let textField = searchController?.searchBar.searchTextField else { + return + } + if loading { + let spinner = UIActivityIndicatorView(style: .medium) + spinner.startAnimating() + textField.rightView = spinner + textField.rightViewMode = .always + } else { + textField.rightView = nil + } + } + + // MARK: - TAP EVENT + + func tapRecommendations(with metadata: tableMetadata) { + didSelectMetadata(metadata, withOcIds: false) + } + + func tapButtonSwitch(_ sender: Any) { + guard !isTransitioning else { return } + isTransitioning = true + + guard let layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) else { return } + + if layoutForView.layout == NCGlobal.shared.layoutGrid { + layoutForView.layout = NCGlobal.shared.layoutList + } else { + layoutForView.layout = NCGlobal.shared.layoutGrid + } + self.layoutForView = NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) + self.collectionView.reloadData() + self.collectionView.collectionViewLayout.invalidateLayout() + self.collectionView.setCollectionViewLayout(layoutForView.layout == NCGlobal.shared.layoutList ? self.listLayout : self.gridLayout, animated: true) {_ in self.isTransitioning = false } + } + + func tapButtonOrder(_ sender: Any) { + +// if let titleButtonHeader = NCKeychain().getTitleButtonHeader(account: session.account), !titleButtonHeader.isEmpty { +// layoutForView?.titleButtonHeader = titleButtonHeader // } // } @@ -888,6 +1424,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func longPressListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + + func tapShareListItem(with objectId: String, indexPath: IndexPath, sender: Any) { } func longPressMoreGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } @@ -917,123 +1457,29 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, }) } - func openMenuItems(with objectId: String?, gestureRecognizer: UILongPressGestureRecognizer) { - if gestureRecognizer.state != .began { return } - - var listMenuItems: [UIMenuItem] = [] - let touchPoint = gestureRecognizer.location(in: collectionView) - - becomeFirstResponder() - - if !serverUrl.isEmpty { - listMenuItems.append(UIMenuItem(title: NSLocalizedString("_paste_file_", comment: ""), action: #selector(pasteFilesMenu(_:)))) - } - - if !listMenuItems.isEmpty { - UIMenuController.shared.menuItems = listMenuItems - UIMenuController.shared.showMenu(from: collectionView, rect: CGRect(x: touchPoint.x, y: touchPoint.y, width: 0, height: 0)) - } - } - - // MARK: - Menu Item - - override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - if #selector(pasteFilesMenu(_:)) == action { - if !UIPasteboard.general.items.isEmpty, !(metadataFolder?.e2eEncrypted ?? false) { - return true - } - } else if #selector(copyMenuFile(_:)) == action { - return true - } else if #selector(moveMenuFile(_:)) == action { - return true - } - - return false - } - - @objc func pasteFilesMenu(_ sender: Any?) { - Task {@MainActor in - guard let tblAccount = await NCManageDatabase.shared.getTableAccountAsync(account: session.account) else { - return - } - let scene = SceneManager.shared.getWindow(controller: controller)?.windowScene - let token = showHudBanner( - scene: scene, - title: NSLocalizedString("_upload_in_progress_", comment: "")) - - for (index, items) in UIPasteboard.general.items.enumerated() { - for item in items { - let capabilities = await NKCapabilities.shared.getCapabilities(for: session.account) - let results = NKFilePropertyResolver().resolve(inUTI: item.key, capabilities: capabilities) - guard let data = UIPasteboard.general.data(forPasteboardType: item.key, - inItemSet: IndexSet([index]))?.first - else { - continue - } - let fileName = results.name + "_" + NCPreferences().incrementalNumber + "." + results.ext - let serverUrlFileName = utilityFileSystem.createServerUrl(serverUrl: serverUrl, fileName: fileName) - let ocIdUpload = UUID().uuidString - let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId( - ocIdUpload, - fileName: fileName, - userId: tblAccount.userId, - urlBase: tblAccount.urlBase - ) - do { - try data.write(to: URL(fileURLWithPath: fileNameLocalPath)) - } catch { - continue - } - - let resultsUpload = await NCNetworking.shared.uploadFile(account: session.account, - fileNameLocalPath: fileNameLocalPath, - serverUrlFileName: serverUrlFileName) { _ in - } progressHandler: { _, _, fractionCompleted in - Task {@MainActor in - LucidBanner.shared.update( - payload: LucidBannerPayload.Update(progress: fractionCompleted), - for: token - ) - } - } + // MARK: - DataSource - if resultsUpload.error == .success, - let etag = resultsUpload.etag, - let ocId = resultsUpload.ocId { - let toPath = self.utilityFileSystem.getDirectoryProviderStorageOcId( - ocId, - fileName: fileName, - userId: tblAccount.userId, - urlBase: tblAccount.urlBase) - self.utilityFileSystem.moveFile(atPath: fileNameLocalPath, toPath: toPath) - NCManageDatabase.shared.addLocalFile( - account: session.account, - etag: etag, - ocId: ocId, - fileName: fileName) - Task { - await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadDataSource(serverUrl: self.serverUrl, requestData: true, status: nil) - } - } - } else { - Task { - await showErrorBanner(scene: scene, text: resultsUpload.error.errorDescription) - } - } - } - } - LucidBanner.shared.dismiss() + @objc func reloadDataSource() async { + + // get auto upload folder + autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() + autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: session.urlBase, userId: session.userId, account: session.account) + + // get layout for view + layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) + // set GroupField for Grid + if !isSearchingMode && layoutForView?.layout == NCGlobal.shared.layoutGrid { + groupByField = "classFile" + } else { + groupByField = "name" } - } - - // MARK: - DataSource - @MainActor - func reloadDataSource() async { - if !isSearchingMode { - Task.detached { - if await self.isRecommendationActived() { + if isSearchingMode { + isDirectoryEncrypted = false + } else { + isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) + if isRecommendationActived { + Task.detached { await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) } } @@ -1043,179 +1489,135 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, delegate.transferReloadData(serverUrl: self.serverUrl) } - await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + await mainNavigationController?.updateMenuOption() + (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + self.refreshControl.endRefreshing() + self.collectionView.reloadData() + self.setNavigationRightItems() + } } func getServerData(forced: Bool = false) async { } - @objc func networkSearch() { - guard !networkSearchInProgress else { - return - } - guard !session.account.isEmpty, - let literalSearch = literalSearch, - !literalSearch.isEmpty else { - return - } - let capabilities = NCNetworking.shared.capabilities[session.account] ?? NKCapabilities.Capabilities() + // MARK: - Push metadata - self.networkSearchInProgress = true - self.dataSource.removeAll() - Task { - await self.reloadDataSource() + /// Pushes or reuses the folder view controller associated with the provided metadata. + /// + /// - Parameter metadata: The metadata representing the selected folder. + @MainActor + func pushMetadata(_ metadata: tableMetadata) async { + guard !isNavigatingMetadata, + let navigationController = self.navigationController, + let navigationCollectionViewCommon = self.controller?.navigationCollectionViewCommon else { + return } - if capabilities.serverVersionMajor >= global.nextcloudVersion20 { - self.networking.unifiedSearchFiles(literal: literalSearch, account: session.account) { task in - self.searchDataSourceTask = task - Task { - await self.reloadDataSource() - } - } providers: { account, searchProviders in - self.providers = searchProviders - self.searchResults = [] - self.dataSource = NCCollectionViewDataSource(metadatas: [], - layoutForView: self.layoutForView, - providers: self.providers, - searchResults: self.searchResults, - account: account) - } update: { _, _, searchResult, metadatas in - guard let metadatas, !metadatas.isEmpty, self.isSearchingMode, let searchResult else { return } - self.networking.unifiedSearchQueue.addOperation(NCCollectionViewUnifiedSearch(collectionViewCommon: self, metadatas: metadatas, searchResult: searchResult)) - } completion: { _, _ in - Task { - await self.reloadDataSource() - } - self.networkSearchInProgress = false - } - } else { - self.networking.searchFiles(literal: literalSearch, account: session.account) { task in - self.searchDataSourceTask = task - Task { - await self.reloadDataSource() - } - } completion: { metadatasSearch, error in - Task { - guard let metadatasSearch, - error == .success, - self.isSearchingMode - else { - self.networkSearchInProgress = false - await self.reloadDataSource() - return - } - let ocId = metadatasSearch.map { $0.ocId } - let metadatas = await self.database.getMetadatasAsync(predicate: NSPredicate(format: "ocId IN %@", ocId), - withLayout: self.layoutForView, - withAccount: self.session.account) - - self.dataSource = NCCollectionViewDataSource(metadatas: metadatas, - layoutForView: self.layoutForView, - providers: self.providers, - searchResults: self.searchResults, - account: self.session.account) - self.networkSearchInProgress = false - await self.reloadDataSource() - } - } + isNavigatingMetadata = true + defer { + isNavigatingMetadata = false } - } - func unifiedSearchMore(metadataForSection: NCMetadataForSection?) { - guard let metadataForSection = metadataForSection, let lastSearchResult = metadataForSection.lastSearchResult, let cursor = lastSearchResult.cursor, let term = literalSearch else { return } + let serverUrlPush = utilityFileSystem.createServerUrl( + serverUrl: metadata.serverUrl, + fileName: metadata.fileName + ) - metadataForSection.unifiedSearchInProgress = true - Task { - await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: nil) - } + // Update the last opening date without blocking the main flow. + Task.detached(priority: .utility) { [database] in + await database.setDirectoryLastOpeningDateAsync(ocId: metadata.ocId) } - self.networking.unifiedSearchFilesProvider(id: lastSearchResult.id, term: term, limit: 5, cursor: cursor, account: session.account) { task in - self.searchDataSourceTask = task - Task { - await self.reloadDataSource() - } - } completion: { _, searchResult, metadatas, error in - if error != .success { - Task { - await showErrorBanner(controller: self.controller, text: error.errorDescription) - } - } + guard navigationController.transitionCoordinator == nil else { + return + } - metadataForSection.unifiedSearchInProgress = false - guard let searchResult = searchResult, let metadatas = metadatas else { return } - self.dataSource.appendMetadatasToSection(metadatas, metadataForSection: metadataForSection, lastSearchResult: searchResult) + if let existingEntry = navigationCollectionViewCommon.first(where: { + $0.navigationController === navigationController && $0.serverUrl == serverUrlPush + }) { + let viewController = existingEntry.viewController - Task { - await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: nil) - } + if navigationController.topViewController === viewController { + return } - } - } - // MARK: - Push metadata + if navigationController.viewControllers.contains(where: { $0 === viewController }) { + navigationController.popToViewController(viewController, animated: true) + return + } - func pushMetadata(_ metadata: tableMetadata) { - guard let navigationCollectionViewCommon = self.controller?.navigationCollectionViewCommon else { + navigationController.pushViewController(viewController, animated: true) return } - let serverUrlPush = utilityFileSystem.createServerUrl(serverUrl: metadata.serverUrl, fileName: metadata.fileName) - // Set Last Opening Date - Task { - await database.setDirectoryLastOpeningDateAsync(ocId: metadata.ocId) + guard let viewController = UIStoryboard(name: "NCFiles", bundle: nil).instantiateInitialViewController() as? NCFiles else { + return } - if let viewController = navigationCollectionViewCommon.first(where: { $0.navigationController == self.navigationController && $0.serverUrl == serverUrlPush})?.viewController, viewController.isViewLoaded { - navigationController?.pushViewController(viewController, animated: true) - } else { - if let viewController: NCFiles = UIStoryboard(name: "NCFiles", bundle: nil).instantiateInitialViewController() as? NCFiles { - viewController.serverUrl = serverUrlPush - viewController.titlePreviusFolder = navigationItem.title - viewController.titleCurrentFolder = metadata.fileNameView + viewController.serverUrl = serverUrlPush + viewController.titlePreviusFolder = navigationItem.title + viewController.titleCurrentFolder = metadata.fileNameView - navigationCollectionViewCommon.append(NavigationCollectionViewCommon(serverUrl: serverUrlPush, navigationController: self.navigationController, viewController: viewController)) + navigationCollectionViewCommon.append( + NavigationCollectionViewCommon( + serverUrl: serverUrlPush, + navigationController: navigationController, + viewController: viewController + ) + ) - navigationController?.pushViewController(viewController, animated: true) - } - } + navigationController.pushViewController(viewController, animated: true) } - // MARK: - Header size - - func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, - heightHeaderRecommendations: CGFloat, - heightHeaderSection: CGFloat) { - var heightHeaderRichWorkspace: CGFloat = 0 - var heightHeaderRecommendations: CGFloat = 0 - var heightHeaderSection: CGFloat = 0 - - if showDescription, - !isSearchingMode, - let richWorkspaceText = self.richWorkspaceText, - !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { - heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 - } + func pushViewController(viewController: UIViewController) { + if pushed { return } - if isRecommendationActived, - !isSearchingMode, - NCPreferences().showRecommendedFiles, - !self.database.getRecommendedFiles(account: self.session.account).isEmpty { - heightHeaderRecommendations = self.heightHeaderRecommendations - heightHeaderSection = self.heightHeaderSection - } + pushed = true + navigationController?.pushViewController(viewController, animated: true) + } + + // MARK: - Header size - if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { - if section == 0 { - return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) - } else { - return (0, 0, self.heightHeaderSection) - } - } else { - return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) +// func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, +// heightHeaderRecommendations: CGFloat, +// heightHeaderSection: CGFloat) { +// var heightHeaderRichWorkspace: CGFloat = 0 +// var heightHeaderRecommendations: CGFloat = 0 +// var heightHeaderSection: CGFloat = 0 +// +// if showDescription, +// !isSearchingMode, +// let richWorkspaceText = self.richWorkspaceText, +// !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { +// heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 +// } +// +// if isRecommendationActived, +// !isSearchingMode, +// NCKeychain().showRecommendedFiles, +// !self.database.getRecommendedFiles(account: self.session.account).isEmpty { +// heightHeaderRecommendations = self.heightHeaderRecommendations +// heightHeaderSection = self.heightHeaderSection +// } +// +// if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { +// if section == 0 { +// return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) +// } else { +// return (0, 0, self.heightHeaderSection) +// } +// } else { +// return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) +// } +// } + + func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { + if headerMenuTransferView, + NCNetworking.shared.isOnline, + let results = database.getResultsMetadatas(predicate: NSPredicate(format: "status IN %@", [global.metadataStatusWaitUpload, global.metadataStatusUploading])), + !results.isEmpty { + return Array(results) } + return nil } func sizeForHeaderInSection(section: Int) -> CGSize { @@ -1224,7 +1626,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let isIphone = UIDevice.current.userInterfaceIdiom == .phone if self.dataSource.isEmpty() { - height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset) + height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset, isHeaderMenuTransferViewEnabled: isHeaderMenuTransferViewEnabled() != nil) } else if isEditMode || (isLandscape && isIphone) { return CGSize.zero } else { @@ -1384,16 +1786,22 @@ extension NCCollectionViewCommon: NCSectionFirstHeaderDelegate { } func tapRecommendations(with metadata: tableMetadata) { - didSelectMetadata(metadata, withOcIds: false) + Task { + await didSelectMetadata(metadata, withOcIds: false) + } } } extension NCCollectionViewCommon: NCSectionFooterDelegate { func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { - unifiedSearchMore(metadataForSection: metadataForSection) + Task { + await unifiedSearchMore(metadataForSection: metadataForSection) + } } } +// MARK: - Transfer Delegate + extension NCCollectionViewCommon: NCTransferDelegate { func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } @@ -1416,28 +1824,37 @@ extension NCCollectionViewCommon: NCTransferDelegate { Task { if error != .success, error.errorCode != global.errorResourceNotFound { - await showErrorBanner(controller: self.controller, text: error.errorDescription) + await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) } + guard session.account == account else { return } - if status == self.global.networkingStatusCreateFolder { - if serverUrl == self.serverUrl, - selector != self.global.selectorUploadAutoUpload, - let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { - self.pushMetadata(metadata) + if self.isSearchingMode { + await self.debouncerNetworkSearch.call { + await self.search() } return } - if self.isSearchingMode { - await self.debouncerNetworkSearch.call { - self.networkSearch() + switch status { + case self.global.networkingStatusCreateFolder: + if error == .success, + serverUrl == self.serverUrl, + selector != self.global.selectorUploadAutoUpload, + let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { + if metadata.e2eEncrypted { + await self.reloadDataSource() + } else { + await self.pushMetadata(metadata) + } } - } else if self.serverUrl == serverUrl || destination == self.serverUrl || self.serverUrl.isEmpty { - await self.debouncerReloadDataSource.call { - await self.reloadDataSource() + default: + if self.serverUrl == serverUrl || destination == self.serverUrl || self.serverUrl.isEmpty { + await self.debouncerReloadDataSource.call { + await self.reloadDataSource() + } } } } @@ -1447,7 +1864,7 @@ extension NCCollectionViewCommon: NCTransferDelegate { Task { if self.isSearchingMode { await self.debouncerNetworkSearch.call { - self.networkSearch() + await self.search() } return } From 43381b34229c19c34fa41ccde5ea0fd838c3e64c Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Tue, 30 Sep 2025 22:00:01 +0530 Subject: [PATCH 34/40] NMC 1984 - Privacy policy customisation added --- .../NCCollectionViewCommon.swift | 550 +++++++++++------- 1 file changed, 353 insertions(+), 197 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index d7e593d66f..8fb30bcbc4 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -4,26 +4,25 @@ import UIKit import SwiftUI +import Realm import RealmSwift import NextcloudKit import EasyTipView import LucidBanner +import MoEngageInApps -class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { -//class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionHeaderMenuDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate, NCEmptyDataSetDelegate { +class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionHeaderMenuDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate, NCEmptyDataSetDelegate { @IBOutlet weak var collectionView: UICollectionView! - internal let database = NCManageDatabase.shared - internal let global = NCGlobal.shared - internal let utility = NCUtility() - internal let utilityFileSystem = NCUtilityFileSystem() - internal let imageCache = NCImageCache.shared - internal var dataSource = NCCollectionViewDataSource() - internal let networking = NCNetworking.shared - internal let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! - internal var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() - private var isNavigatingMetadata = false + let database = NCManageDatabase.shared + let global = NCGlobal.shared + let utility = NCUtility() + let utilityFileSystem = NCUtilityFileSystem() + let imageCache = NCImageCache.shared + var dataSource = NCCollectionViewDataSource() + let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! + var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() var autoUploadFileName = "" var autoUploadDirectory = "" @@ -54,9 +53,17 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, var tabBarSelect: NCCollectionViewCommonSelectTabBar? var attributesZoomIn: UIMenuElement.Attributes = [] var attributesZoomOut: UIMenuElement.Attributes = [] - var tipViewAccounts: EasyTipView? var syncMetadatasTask: Task? + let maxImageGrid: CGFloat = 7 +// var headerMenu: NCSectionFirstHeader? + var tipViewAccounts: EasyTipView? + var tipViewAutoUpload: EasyTipView? + var headerMenu: NCSectionHeaderMenu? + var headerMenuTransferView = false + var headerMenuButtonsView: Bool = true + var headerRichWorkspaceDisable: Bool = false + // DECLARE var layoutKey = "" var titleCurrentFolder = "" @@ -97,6 +104,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let heightHeaderRecommendations: CGFloat = 160 let heightHeaderSection: CGFloat = 30 + var session: NCSession.Session { + NCSession.shared.getSession(controller: tabBarController) + } + internal var isLayoutPhoto: Bool { layoutForView?.layout == global.layoutPhotoRatio || layoutForView?.layout == global.layoutPhotoSquare } @@ -142,6 +153,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, (self.tabBarController as? NCMainTabBarController)?.sceneIdentifier ?? "" } + var defaultPredicate: NSPredicate { + let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, NCGlobal.shared.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) + return predicate + } + + var personalFilesOnlyPredicate: NSPredicate { + let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND (ownerId == %@ || ownerId == '') AND mountType == '' AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, session.userId, global.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) + return predicate + } + @MainActor internal var windowScene: UIWindowScene? { SceneManager.shared.getWindowScene(controller: self.tabBarController as? NCMainTabBarController) @@ -182,6 +203,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, override func viewDidLoad() { super.viewDidLoad() + tabBarSelect = NCCollectionViewCommonSelectTabBar(controller: self.controller, delegate: self) self.navigationController?.presentationController?.delegate = self collectionView.alwaysBounceVertical = true collectionView.accessibilityIdentifier = "NCCollectionViewCommon" @@ -239,6 +261,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // Wait 1.5 seconds before resetting the button alpha try? await Task.sleep(for: .seconds(1.5)) self.mainNavigationController?.menuPlus?.resetPlusButtonAlpha() +// self.dataSource.removeAll() +// self.getServerData() +// if self.isRecommendationActived { +// Task.detached { +// await NCNetworking.shared.createRecommendations(session: self.session) +// } +// } +// self.refreshControl.endRefreshing() +// DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { +// self.resetPlusButtonAlpha() } } @@ -289,6 +321,12 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, await self.debouncerReloadData.resume() } } + + NotificationCenter.default.addObserver(self, selector: #selector(changeTheming(_:)), name: NSNotification.Name(rawValue: global.notificationCenterChangeTheming), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(reloadDataSource(_:)), name: NSNotification.Name(rawValue: global.notificationCenterReloadDataSource), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(getServerData(_:)), name: NSNotification.Name(rawValue: global.notificationCenterGetServerData), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(reloadHeader(_:)), name: NSNotification.Name(rawValue: global.notificationCenterReloadHeader), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(updateIcons), name: NSNotification.Name(rawValue: global.notificationCenterUpdateIcons), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateIcons), name: NSNotification.Name(rawValue: global.notificationCenterUpdateIcons), object: nil) @@ -318,6 +356,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + AnalyticsHelper.shared.displayInAppNotification() + if titlePreviusFolder != nil { navigationController?.navigationBar.topItem?.title = titlePreviusFolder } @@ -327,11 +367,12 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, navigationController?.setNavigationBarHidden(false, animated: true) appDelegate.activeViewController = self + appDelegate.account = session.account + appDelegate.urlBase = session.urlBase + appDelegate.userId = session.userId + appDelegate.user = session.user - if tabBarSelect == nil { - tabBarSelect = NCCollectionViewCommonSelectTabBar(controller: self.controller, viewController: self, delegate: self) - } - + NCKeychain().setAccountName(account: session.account) isEditMode = false Task { @@ -402,6 +443,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + + NCNetworking.shared.cancelUnifiedSearchFiles() dismissTip() pushed = false toggleSelect(isOn: false) @@ -410,14 +453,14 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, Task { await searchOperationHandle.cancel() } + NCNetworking.shared.unifiedSearchQueue.cancelAll() + dataSourceTask?.cancel() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - Task { - await NCNetworking.shared.transferDispatcher.removeDelegate(self) - } + NCNetworking.shared.transferDelegate = nil NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCloseRichWorkspaceWebView), object: nil) @@ -432,6 +475,11 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCreateFolder), object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterFavoriteFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterDeleteFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterMoveFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterCopyFile), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterCreateFolder), object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterFavoriteFile), object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDownloadStartFile), object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDownloadedFile), object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterDownloadCancelFile), object: nil) @@ -443,7 +491,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterUpdateShare), object: nil) - removeImageCache(metadatas: self.dataSource.getMetadatas()) + dataSource.removeImageCache() } func isApplicationUpdated() -> Bool { @@ -475,7 +523,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { let viewController = presentationController.presentedViewController - + if viewController is NCViewerRichWorkspaceWebView { closeRichWorkspaceWebView() } @@ -691,17 +739,29 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, reloadDataSource() } - @objc func closeRichWorkspaceWebView() { - Task { - await self.reloadDataSource() + @objc func reloadAvatar(_ notification: NSNotification) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.showTipAccounts() } + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError, + error.errorCode != global.errorNotModified else { return } + /// Magentacloud branding changes hide user account button on left navigation bar + setNavigationLeftItems() } - // MARK: - Layout + @objc func changeTheming(_ notification: NSNotification) { + self.reloadDataSource() + } - func changeLayout(layoutForView: NCDBLayoutForView) { - let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) - let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 + @objc func changeLayout(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let layoutForView = userInfo["layoutForView"] as? NCDBLayoutForView, + account == session.account, + serverUrl == self.serverUrl + else { return } if serverUrl == homeServer || numFoldersLayoutsForView == 1 { Task { @@ -723,9 +783,29 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, await self.reloadDataSource() } })) +// if self.layoutForView?.layout == layoutForView.layout { +// self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) +// self.reloadDataSource() +// return +// } +// +// self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) +// layoutForView.layout = layoutForView.layout +// self.layoutType = layoutForView.layout +// +// collectionView.reloadData() - self.present(alertController, animated: true) + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break } + self.collectionView.collectionViewLayout.invalidateLayout() // (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() @@ -1011,16 +1091,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, collectionView?.reloadData() } } - - @objc func updateShare(_ notification: NSNotification) { - if isSearchingMode { - networkSearch() - } else { - self.dataSource.removeAll() - getServerData() - } - } - + @objc func triggerProgressTask(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, let progressNumber = userInfo["progress"] as? NSNumber, @@ -1035,14 +1106,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) - - // HEADER -// if self.headerMenuTransferView, transfer.session.contains("upload") { -// self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) -// self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) -// } - DispatchQueue.main.async { +// DispatchQueue.main.async { if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { if NCNetworking.shared.transferInForegorund?.ocId == ocId { NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue @@ -1052,7 +1117,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, + guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId), let cell = self.collectionView?.cellForItem(at: indexPath), let cell = cell as? NCCellProtocol else { return } if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { @@ -1084,8 +1149,22 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } } } +// } + } + + @objc func updateShare(_ notification: NSNotification) { + if isSearchingMode { + networkSearch() + } else { +// self.dataSource.removeAll() + getServerData() } } + + @objc func updateIcons() { + collectionView.reloadData() +// reloadDataSource() + } // MARK: - Layout @@ -1094,14 +1173,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } func getNavigationTitle() -> String { - let tblAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) - if let tblAccount, - !tblAccount.alias.isEmpty { - return tblAccount.alias + let tableAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) + if let tableAccount, + !tableAccount.alias.isEmpty { + return tableAccount.alias } return NCBrandOptions.shared.brand } + func accountSettingsDidDismiss(tableAccount: tableAccount?, controller: NCMainTabBarController?) { } + @MainActor func startGUIGetServerData() { self.dataSource.setGetServerData(false) @@ -1134,6 +1215,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.navigationItem.titleView = nil self.navigationItem.title = self.titleCurrentFolder } + + func resetPlusButtonAlpha(animated: Bool = true) { } + + func isHiddenPlusButton(_ isHidden: Bool) { } // MARK: - Empty @@ -1196,6 +1281,12 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.dataSource.removeAll() self.collectionView.reloadData() } +// isSearchingMode = true +// self.providers?.removeAll() +// self.dataSource.removeAll() +// self.reloadDataSource() +// // TIP +// dismissTip() } func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { @@ -1210,17 +1301,12 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - self.networking.cancelUnifiedSearchFiles() - + NCNetworking.shared.cancelUnifiedSearchFiles() self.isSearchingMode = false self.literalSearch = "" self.providers?.removeAll() self.dataSource.removeAll() - Task { - await self.reloadDataSource() - } - // - mainNavigationController?.hiddenPlusButton(false) + self.reloadDataSource() } // MARK: - TAP EVENT @@ -1228,6 +1314,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, func tapMoreListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) } + + func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) + } func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) @@ -1255,6 +1345,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } } } + + func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } +// toggleMenu(metadata: metadata, image: image) + Task { + await cancelSession(metadata: metadata) + } + } + + func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } func tapRichWorkspace(_ sender: Any) { if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { @@ -1272,16 +1372,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { // (+) self.mainNavigationController?.menuPlus?.hiddenPlusButton(false) - + self.isSearchingMode = false self.networkSearchInProgress = false self.searchResultText = nil self.searchResultStore = nil - + Task { await searchOperationHandle.cancel() await reloadDataSource() - + // Restore Layout if let layoutForViewLayoutStore { let layoutForView = database.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) @@ -1289,12 +1389,16 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, await setLayout(layoutForView: layoutForView) } layoutForViewLayoutStore = nil - + // update Option menu await mainNavigationController?.updateMenuOption() } } + func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?) { + toggleMenu(metadata: metadata, image: image) + } + @MainActor func setSearchBarLoading(_ loading: Bool) { guard let textField = searchController?.searchBar.searchTextField else { @@ -1334,100 +1438,19 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } func tapButtonOrder(_ sender: Any) { - -// if let titleButtonHeader = NCKeychain().getTitleButtonHeader(account: session.account), !titleButtonHeader.isEmpty { -// layoutForView?.titleButtonHeader = titleButtonHeader -// } -// } - -// func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { -// toggleMenu(metadata: metadata, image: image, sender: sender) -// } - -// func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { -// unifiedSearchMore(metadataForSection: metadataForSection) -// } - - func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { - unifiedSearchMore(metadataForSection: metadataForSection) + let sortMenu = NCSortMenu() + sortMenu.toggleMenu(viewController: self, account: session.account, key: layoutKey, sortButton: sender as? UIButton, serverUrl: serverUrl) } - @MainActor - func setSearchBarLoading(_ loading: Bool) { - guard let textField = searchController?.searchBar.searchTextField else { - return - } - if loading { - let spinner = UIActivityIndicatorView(style: .medium) - spinner.startAnimating() - textField.rightView = spinner - textField.rightViewMode = .always - } else { - textField.rightView = nil - } - // - mainNavigationController?.hiddenPlusButton(false) - } - - // MARK: - TAP EVENT - - func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { - tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) - } - - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { - tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) - } - - func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) { - guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } - TealiumHelper.shared.trackEvent(title: "magentacloud-app.filebrowser.sharing", data: ["": ""]) - appDelegate.adjust.trackEvent(TriggerEvent(Sharing.rawValue)) - - NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) - } - - func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { - guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } - toggleMenu(metadata: metadata, image: image, sender: sender) - } - -// func tapRichWorkspace(_ sender: Any) { -// if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { -// if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { -// viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" -// viewerRichWorkspace.serverUrl = serverUrl -// viewerRichWorkspace.delegate = self -// -// navigationController.modalPresentationStyle = .fullScreen -// self.present(navigationController, animated: true, completion: nil) -// } -// } -// } - -// func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { -// toggleMenu(metadata: metadata, image: image, sender: sender) -// } - -// func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { -// unifiedSearchMore(metadataForSection: metadataForSection) -// } - -// func tapRecommendations(with metadata: tableMetadata) { -// didSelectMetadata(metadata, withOcIds: false) -// } - + func tapButtonTransfer(_ sender: Any) { } + + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } + func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func longPressGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - - func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - - func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - + func longPressListItem(with objectId: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) { + } func longPressListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } - - func tapShareListItem(with objectId: String, indexPath: IndexPath, sender: Any) { } func longPressMoreGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } @@ -1457,14 +1480,130 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, }) } + func openMenuItems(with objectId: String?, gestureRecognizer: UILongPressGestureRecognizer) { + if gestureRecognizer.state != .began { return } + + var listMenuItems: [UIMenuItem] = [] + let touchPoint = gestureRecognizer.location(in: collectionView) + + becomeFirstResponder() + + if !serverUrl.isEmpty { + listMenuItems.append(UIMenuItem(title: NSLocalizedString("_paste_file_", comment: ""), action: #selector(pasteFilesMenu))) + } + + if !listMenuItems.isEmpty { + UIMenuController.shared.menuItems = listMenuItems + UIMenuController.shared.showMenu(from: collectionView, rect: CGRect(x: touchPoint.x, y: touchPoint.y, width: 0, height: 0)) + } + } + + // MARK: - Transfer Delegate + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + + func tranferChange(status: String, metadata: tableMetadata, error: NKError) { } + + // MARK: - Menu Item + + override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + + if #selector(pasteFilesMenu) == action { + if !UIPasteboard.general.items.isEmpty, !(metadataFolder?.e2eEncrypted ?? false) { + return true + } + } else if #selector(copyMenuFile) == action { + return true + } else if #selector(moveMenuFile) == action { + return true + } + + return false + } + + @objc func pasteFilesMenu(_ sender: Any?) { + Task {@MainActor in + guard let tblAccount = await NCManageDatabase.shared.getTableAccountAsync(account: session.account) else { + return + } + let scene = SceneManager.shared.getWindow(controller: controller)?.windowScene + let token = showHudBanner( + scene: scene, + title: NSLocalizedString("_upload_in_progress_", comment: "")) + + for (index, items) in UIPasteboard.general.items.enumerated() { + for item in items { + let capabilities = await NKCapabilities.shared.getCapabilities(for: session.account) + let results = NKFilePropertyResolver().resolve(inUTI: item.key, capabilities: capabilities) + guard let data = UIPasteboard.general.data(forPasteboardType: item.key, + inItemSet: IndexSet([index]))?.first + else { + continue + } + let fileName = results.name + "_" + NCPreferences().incrementalNumber + "." + results.ext + let serverUrlFileName = utilityFileSystem.createServerUrl(serverUrl: serverUrl, fileName: fileName) + let ocIdUpload = UUID().uuidString + let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId( + ocIdUpload, + fileName: fileName, + userId: tblAccount.userId, + urlBase: tblAccount.urlBase + ) + do { + try data.write(to: URL(fileURLWithPath: fileNameLocalPath)) + } catch { + continue + } + + let resultsUpload = await NCNetworking.shared.uploadFile(account: session.account, + fileNameLocalPath: fileNameLocalPath, + serverUrlFileName: serverUrlFileName) { _ in + } progressHandler: { _, _, fractionCompleted in + Task {@MainActor in + LucidBanner.shared.update(progress: fractionCompleted, for: token) + } + } + + if resultsUpload.error == .success, + let etag = resultsUpload.etag, + let ocId = resultsUpload.ocId { + let toPath = self.utilityFileSystem.getDirectoryProviderStorageOcId( + ocId, + fileName: fileName, + userId: tblAccount.userId, + urlBase: tblAccount.urlBase) + self.utilityFileSystem.moveFile(atPath: fileNameLocalPath, toPath: toPath) + NCManageDatabase.shared.addLocalFile( + account: session.account, + etag: etag, + ocId: ocId, + fileName: fileName) + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: serverUrl, requestData: true, status: nil) + } + } + } else { + Task {@MainActor in + await showErrorBanner(scene: scene, + errorDescription: resultsUpload.error.errorDescription, + errorCode: resultsUpload.error.errorCode) + } + } + } + } + LucidBanner.shared.dismiss() + } + } + // MARK: - DataSource - @objc func reloadDataSource() async { + @objc func reloadDataSource() { // get auto upload folder autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: session.urlBase, userId: session.userId, account: session.account) - + // get layout for view layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) // set GroupField for Grid @@ -1473,16 +1612,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } else { groupByField = "name" } - if isSearchingMode { isDirectoryEncrypted = false } else { isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) - if isRecommendationActived { - Task.detached { - await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) - } - } } await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in @@ -1497,7 +1630,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } } - func getServerData(forced: Bool = false) async { } + func getServerData() { + } // MARK: - Push metadata @@ -1567,38 +1701,55 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, navigationController.pushViewController(viewController, animated: true) } - + func pushViewController(viewController: UIViewController) { if pushed { return } pushed = true navigationController?.pushViewController(viewController, animated: true) } - + // MARK: - Header size -// func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, -// heightHeaderRecommendations: CGFloat, -// heightHeaderSection: CGFloat) { -// var heightHeaderRichWorkspace: CGFloat = 0 -// var heightHeaderRecommendations: CGFloat = 0 -// var heightHeaderSection: CGFloat = 0 +// func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { +// var headerRichWorkspace: CGFloat = 0 // -// if showDescription, -// !isSearchingMode, -// let richWorkspaceText = self.richWorkspaceText, -// !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { -// heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 -// } +// func getHeaderHeight() -> CGFloat { +// var size: CGFloat = 0 // -// if isRecommendationActived, -// !isSearchingMode, -// NCKeychain().showRecommendedFiles, -// !self.database.getRecommendedFiles(account: self.session.account).isEmpty { -// heightHeaderRecommendations = self.heightHeaderRecommendations -// heightHeaderSection = self.heightHeaderSection +// if isHeaderMenuTransferViewEnabled() != nil { +// if !isSearchingMode { +// size += global.heightHeaderTransfer +// } +// } +// if headerMenuButtonsView { +// size += NCGlobal.shared.heightButtonsView +// } +// return size // } // +//// func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, +//// heightHeaderRecommendations: CGFloat, +//// heightHeaderSection: CGFloat) { +//// var heightHeaderRichWorkspace: CGFloat = 0 +//// var heightHeaderRecommendations: CGFloat = 0 +//// var heightHeaderSection: CGFloat = 0 +//// +//// if showDescription, +//// !isSearchingMode, +//// let richWorkspaceText = self.richWorkspaceText, +//// !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { +//// heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 +//// } +//// +//// if isRecommendationActived, +//// !isSearchingMode, +//// NCKeychain().showRecommendedFiles, +//// !self.database.getRecommendedFiles(account: self.session.account).isEmpty { +//// heightHeaderRecommendations = self.heightHeaderRecommendations +//// heightHeaderSection = self.heightHeaderSection +//// } +// // if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { // if section == 0 { // return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) @@ -1607,6 +1758,15 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // } // } else { // return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) +//// +//// if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { +//// if section == 0 { +//// return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) +//// } else { +//// return (0, 0, self.heightHeaderSection) +//// } +//// } else { +//// return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) // } // } @@ -1630,8 +1790,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } else if isEditMode || (isLandscape && isIphone) { return CGSize.zero } else { - let (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) = getHeaderHeight(section: section) - height = heightHeaderRichWorkspace + heightHeaderRecommendations + heightHeaderSection + let (heightHeaderCommands, heightHeaderRichWorkspace, heightHeaderSection) = getHeaderHeight(section: section) + height = heightHeaderCommands + heightHeaderRichWorkspace + heightHeaderSection } return CGSize(width: collectionView.frame.width, height: height) @@ -1640,23 +1800,14 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, // MARK: - Footer size func sizeForFooterInSection(section: Int) -> CGSize { - guard let controller else { - return CGSize.zero - } let sections = dataSource.numberOfSections() - let bottomAreaInsets: CGFloat = controller.tabBar.safeAreaInsets.bottom == 0 ? 34 : 0 - let height = controller.tabBar.frame.height + bottomAreaInsets - - if isEditMode { - return CGSize(width: collectionView.frame.width, height: 90 + height) - } - - if isSearchingMode { - return CGSize(width: collectionView.frame.width, height: 50) - } + let metadataForSection = self.dataSource.getMetadataForSection(section) + let isPaginated = metadataForSection?.lastSearchResult?.isPaginated ?? false + let metadatasCount: Int = metadataForSection?.lastSearchResult?.entries.count ?? 0 + var size = CGSize(width: collectionView.frame.width, height: 0) if section == sections - 1 { - return CGSize(width: collectionView.frame.width, height: height) + size.height += 85 } else { size.height += 1 } @@ -1766,6 +1917,11 @@ extension NCCollectionViewCommon: NCTransferDelegate { } } } + + if isSearchingMode && isPaginated && metadatasCount > 0 { + size.height += 30 + } + return size } func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } From 7e65b1dd88d735ae97ceca556f3914bc78a7943b Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:58:19 +0530 Subject: [PATCH 35/40] NMC 1984 - Privacy policy customisation added --- .../Collection Common/NCCollectionViewCommon.swift | 13 +++++++++++++ .../Settings/PrivacySettingsViewController.swift | 11 +---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 8fb30bcbc4..f07aa16326 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -506,6 +506,19 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS newViewController.modalPresentationStyle = .fullScreen self.present(newViewController, animated: true, completion: nil) } + + func isApplicationUpdated() -> Bool{ + let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" + let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") + return currentVersion != appVersion + } + + func redirectToPrivacyViewController(){ + let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) + let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController + newViewController.modalPresentationStyle = .fullScreen + self.present(newViewController, animated: true, completion: nil) + } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index 782f3e76bb..d995fef6fc 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -13,7 +13,7 @@ import XLForm class PrivacySettingsViewController: XLFormViewController{ - @objc public var isShowSettingsButton: Bool = false + @objc public var isShowSettingsButton: Bool = false override func viewDidLoad() { super.viewDidLoad() @@ -35,13 +35,9 @@ class PrivacySettingsViewController: XLFormViewController{ tableView.reloadData() initializeForm() } - - //MARK: XLForm - func initializeForm() { - let form : XLFormDescriptor = XLFormDescriptor() as XLFormDescriptor form.rowNavigationOptions = XLFormRowNavigationOptions.stopDisableRow @@ -58,7 +54,6 @@ class PrivacySettingsViewController: XLFormViewController{ section.footerTitle = NSLocalizedString("_privacy_settings_help_text_", comment: "") form.addFormSection(section) - //custom cell section = XLFormSectionDescriptor.formSection(withTitle: "") section.footerTitle = NSLocalizedString("_required_data_collection_help_text_", comment: "") @@ -67,7 +62,6 @@ class PrivacySettingsViewController: XLFormViewController{ XLFormViewController.cellClassesForRowDescriptorTypes()["RequiredDataCollectionCustomCellType"] = RequiredDataCollectionSwitch.self - row = XLFormRowDescriptor(tag: "ButtonDestinationFolder", rowType: "RequiredDataCollectionCustomCellType", title: "") row.cellConfig["requiredDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue @@ -82,7 +76,6 @@ class PrivacySettingsViewController: XLFormViewController{ XLFormViewController.cellClassesForRowDescriptorTypes()["AnalysisDataCollectionCustomCellType"] = AnalysisDataCollectionSwitch.self - row = XLFormRowDescriptor(tag: "AnalysisDataCollectionSwitch", rowType: "AnalysisDataCollectionCustomCellType", title: "") row.cellConfig["analysisDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue @@ -97,13 +90,11 @@ class PrivacySettingsViewController: XLFormViewController{ section.addFormRow(row) - XLFormViewController.cellClassesForRowDescriptorTypes()["SaveSettingsButton"] = SaveSettingsCustomButtonCell.self section = XLFormSectionDescriptor.formSection(withTitle: "") form.addFormSection(section) - row = XLFormRowDescriptor(tag: "SaveSettingsButton", rowType: "SaveSettingsButton", title: "") row.cellConfig["backgroundColor"] = UIColor.clear From 758c2971373b5a956c5194972e608e01cd1cb7a6 Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Tue, 26 Dec 2023 13:29:37 +0530 Subject: [PATCH 36/40] NMC 1984 - Privacy setting view controller added --- iOSClient/Settings/PrivacySettingsViewController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index d995fef6fc..e4497c92bc 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -35,9 +35,11 @@ class PrivacySettingsViewController: XLFormViewController{ tableView.reloadData() initializeForm() } - + //MARK: XLForm + func initializeForm() { + let form : XLFormDescriptor = XLFormDescriptor() as XLFormDescriptor form.rowNavigationOptions = XLFormRowNavigationOptions.stopDisableRow From a7db5c934cf56a9969a559b3a0c777f427417e63 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Wed, 9 Apr 2025 14:10:39 +0530 Subject: [PATCH 37/40] NMC 1984 - Privacy policy customisation changes added --- .../NCCollectionViewCommon.swift | 464 +++++++++++++++--- 1 file changed, 402 insertions(+), 62 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index f07aa16326..ed258f5629 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -507,13 +507,13 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.present(newViewController, animated: true, completion: nil) } - func isApplicationUpdated() -> Bool{ + func isApplicationUpdated() -> Bool { let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") return currentVersion != appVersion } - func redirectToPrivacyViewController(){ + func redirectToPrivacyViewController() { let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController newViewController.modalPresentationStyle = .fullScreen @@ -1179,6 +1179,399 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // reloadDataSource() } + // MARK: - Layout + + func changeLayout(layoutForView: NCDBLayoutForView) { + let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) + let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 + + func changeLayout(withSubFolders: Bool) { + if self.layoutForView?.layout == layoutForView.layout { + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + Task { + await self.reloadDataSource() + } + return + } + + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + layoutForView.layout = layoutForView.layout + self.layoutType = layoutForView.layout + + collectionView.reloadData() + + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break + } + + self.collectionView.collectionViewLayout.invalidateLayout() + + Task { + await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + } + } + + if serverUrl == homeServer || numFoldersLayoutsForView == 1 { + changeLayout(withSubFolders: false) + } else { + let alertController = UIAlertController(title: NSLocalizedString("_propagate_layout_", comment: ""), message: nil, preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default, handler: { _ in + changeLayout(withSubFolders: true) + })) + alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in + changeLayout(withSubFolders: false) + })) + + self.present(alertController, animated: true) + } + self.collectionView.collectionViewLayout.invalidateLayout() + +// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() + } + + @objc func reloadDataSource(_ notification: NSNotification) { + if let userInfo = notification.userInfo as? NSDictionary { + if let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } + } + + if let clearDataSource = userInfo["clearDataSource"] as? Bool, clearDataSource { + self.dataSource.removeAll() + } + } + + reloadDataSource() + } + + @objc func getServerData(_ notification: NSNotification) { + if let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } + } + + getServerData() + } + + @objc func reloadHeader(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + account == session.account + else { return } + + self.collectionView.reloadData() + } + + @objc func changeStatusFolderE2EE(_ notification: NSNotification) { + reloadDataSource() + } + + @objc func closeRichWorkspaceWebView() { + reloadDataSource() + } + + @objc func deleteFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError else { return } + + if error == .success { + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + } else { + NCContentPresenter().showError(error: error) + } + + reloadDataSource() + } + + @objc func copyMoveFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + account == session.account else { return } + + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + + if serverUrl == self.serverUrl { + reloadDataSource() + } + } + + @objc func renameFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let error = userInfo["error"] as? NKError, + account == session.account + else { return } + + if error == .success { + if isSearchingMode { + return networkSearch() + } + + if isRecommendationActived { + Task.detached { + await NCNetworking.shared.createRecommendations(session: self.session) + } + } + } + + if serverUrl == self.serverUrl { + if error != .success { + NCContentPresenter().showError(error: error) + } + reloadDataSource() + } else { + collectionView.reloadData() + } + } + + @objc func createFolder(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let account = userInfo["account"] as? String, + account == session.account, + let withPush = userInfo["withPush"] as? Bool, + let metadata = database.getMetadataFromOcId(ocId) + else { return } + + if isSearchingMode { + return networkSearch() + } + + if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { + reloadDataSource() + } else if withPush, metadata.serverUrl == self.serverUrl { + reloadDataSource() + if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { + if sceneIdentifier == controller?.sceneIdentifier { + pushMetadata(metadata) + } + } else { + pushMetadata(metadata) + } + } + } + + @objc func favoriteFile(_ notification: NSNotification) { + if isSearchingMode { + return networkSearch() + } + + if self is NCFavorite { + return reloadDataSource() + } + + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + serverUrl == self.serverUrl + else { return } + + reloadDataSource() + } + + @objc func downloadStartFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func downloadedFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func downloadCancelFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadStartFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + !isSearchingMode, + let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) + else { return } + + // Header view trasfer + if metadata.isTransferInForeground { + NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) + DispatchQueue.main.async { self.collectionView?.reloadData() } + } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadedFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadedLivePhoto(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func uploadCancelFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String + else { return } + + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } + } + + @objc func updateShare(_ notification: NSNotification) { + if isSearchingMode { + networkSearch() + } else { + self.dataSource.removeAll() + getServerData() + } + } + + @objc func triggerProgressTask(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let progressNumber = userInfo["progress"] as? NSNumber, + let totalBytes = userInfo["totalBytes"] as? Int64, + let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, + let ocId = userInfo["ocId"] as? String, + let ocIdTransfer = userInfo["ocIdTransfer"] as? String, + let session = userInfo["session"] as? String + else { return } + + let chunk: Int = userInfo["chunk"] as? Int ?? 0 + let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false + + let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) + + // HEADER +// if self.headerMenuTransferView, transfer.session.contains("upload") { +// self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) +// self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) +// } + + DispatchQueue.main.async { + if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { + if NCNetworking.shared.transferInForegorund?.ocId == ocId { + NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue + } else { + NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) + self.collectionView.reloadData() + } + self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue + } else { + guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, + let cell = self.collectionView?.cellForItem(at: indexPath), + let cell = cell as? NCCellProtocol else { return } + if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { + cell.fileProgressView?.isHidden = true + cell.fileProgressView?.progress = .zero + cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) + if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { + cell.writeInfoDateSize(date: metadata.date, size: metadata.size) + } else { + cell.fileInfoLabel?.text = "" + cell.fileSubinfoLabel?.text = "" + } + } else { + cell.fileProgressView?.isHidden = false + cell.fileProgressView?.progress = progressNumber.floatValue + cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) + let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal + if status == NCGlobal.shared.metadataStatusDownloading { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) + } else if status == NCGlobal.shared.metadataStatusUploading { + if totalBytes > 0 { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) + } else { + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" + } + } + } + } + } + } + // MARK: - Layout func setNavigationLeftItems() { @@ -1616,7 +2009,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // get auto upload folder autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: session.urlBase, userId: session.userId, account: session.account) - // get layout for view layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) // set GroupField for Grid @@ -1722,66 +2114,14 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS navigationController?.pushViewController(viewController, animated: true) } - // MARK: - Header size + func pushViewController(viewController: UIViewController) { + if pushed { return } -// func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { -// var headerRichWorkspace: CGFloat = 0 -// -// func getHeaderHeight() -> CGFloat { -// var size: CGFloat = 0 -// -// if isHeaderMenuTransferViewEnabled() != nil { -// if !isSearchingMode { -// size += global.heightHeaderTransfer -// } -// } -// if headerMenuButtonsView { -// size += NCGlobal.shared.heightButtonsView -// } -// return size -// } -// -//// func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, -//// heightHeaderRecommendations: CGFloat, -//// heightHeaderSection: CGFloat) { -//// var heightHeaderRichWorkspace: CGFloat = 0 -//// var heightHeaderRecommendations: CGFloat = 0 -//// var heightHeaderSection: CGFloat = 0 -//// -//// if showDescription, -//// !isSearchingMode, -//// let richWorkspaceText = self.richWorkspaceText, -//// !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { -//// heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 -//// } -//// -//// if isRecommendationActived, -//// !isSearchingMode, -//// NCKeychain().showRecommendedFiles, -//// !self.database.getRecommendedFiles(account: self.session.account).isEmpty { -//// heightHeaderRecommendations = self.heightHeaderRecommendations -//// heightHeaderSection = self.heightHeaderSection -//// } -// -// if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { -// if section == 0 { -// return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) -// } else { -// return (0, 0, self.heightHeaderSection) -// } -// } else { -// return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) -//// -//// if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { -//// if section == 0 { -//// return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) -//// } else { -//// return (0, 0, self.heightHeaderSection) -//// } -//// } else { -//// return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) -// } -// } + pushed = true + navigationController?.pushViewController(viewController, animated: true) + } + + // MARK: - Header size func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { if headerMenuTransferView, From d7a53f744dc79811ff7e13a6e4730ee02a34510b Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Tue, 30 Sep 2025 22:00:01 +0530 Subject: [PATCH 38/40] NMC 1984 - Privacy policy customisation added --- .../NCCollectionViewCommon.swift | 119 +++++++----------- 1 file changed, 46 insertions(+), 73 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index ed258f5629..f619b07794 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -500,19 +500,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return currentVersion != appVersion } - func redirectToPrivacyViewController() { - let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) - let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController - newViewController.modalPresentationStyle = .fullScreen - self.present(newViewController, animated: true, completion: nil) - } - - func isApplicationUpdated() -> Bool { - let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" - let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") - return currentVersion != appVersion - } - func redirectToPrivacyViewController() { let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController @@ -928,7 +915,17 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS if serverUrl == self.serverUrl { reloadDataSource() + } + + @objc func reloadAvatar(_ notification: NSNotification) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.showTipAccounts() } + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError, + error.errorCode != global.errorNotModified else { return } + /// Magentacloud branding changes hide user account button on left navigation bar + setNavigationLeftItems() } @objc func renameFile(_ notification: NSNotification) { @@ -1185,53 +1182,30 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 - func changeLayout(withSubFolders: Bool) { - if self.layoutForView?.layout == layoutForView.layout { - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) - Task { - await self.reloadDataSource() - } - return - } - - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) - layoutForView.layout = layoutForView.layout - self.layoutType = layoutForView.layout - collectionView.reloadData() - - switch layoutForView.layout { - case global.layoutList: - self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) - case global.layoutGrid: - self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) - case global.layoutPhotoSquare, global.layoutPhotoRatio: - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) - default: - break - } - - self.collectionView.collectionViewLayout.invalidateLayout() - - Task { - await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() - } + if self.layoutForView?.layout == layoutForView.layout { + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) + self.reloadDataSource() + return } - if serverUrl == homeServer || numFoldersLayoutsForView == 1 { - changeLayout(withSubFolders: false) - } else { - let alertController = UIAlertController(title: NSLocalizedString("_propagate_layout_", comment: ""), message: nil, preferredStyle: .alert) + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) + layoutForView.layout = layoutForView.layout + self.layoutType = layoutForView.layout - alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default, handler: { _ in - changeLayout(withSubFolders: true) - })) - alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in - changeLayout(withSubFolders: false) - })) + collectionView.reloadData() - self.present(alertController, animated: true) + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break } + self.collectionView.collectionViewLayout.invalidateLayout() // (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() @@ -1496,16 +1470,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS collectionView?.reloadData() } } - - @objc func updateShare(_ notification: NSNotification) { - if isSearchingMode { - networkSearch() - } else { - self.dataSource.removeAll() - getServerData() - } - } - + @objc func triggerProgressTask(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, let progressNumber = userInfo["progress"] as? NSNumber, @@ -1520,14 +1485,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) - - // HEADER -// if self.headerMenuTransferView, transfer.session.contains("upload") { -// self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) -// self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) -// } - DispatchQueue.main.async { +// DispatchQueue.main.async { if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { if NCNetworking.shared.transferInForegorund?.ocId == ocId { NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue @@ -1537,7 +1496,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, + guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId), let cell = self.collectionView?.cellForItem(at: indexPath), let cell = cell as? NCCellProtocol else { return } if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { @@ -1569,8 +1528,22 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } } +// } + } + + @objc func updateShare(_ notification: NSNotification) { + if isSearchingMode { + networkSearch() + } else { +// self.dataSource.removeAll() + getServerData() } } + + @objc func updateIcons() { + collectionView.reloadData() +// reloadDataSource() + } // MARK: - Layout @@ -2120,7 +2093,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS pushed = true navigationController?.pushViewController(viewController, animated: true) } - + // MARK: - Header size func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { From 89ead911a64634677938a3ea89408cfca3efb4db Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Wed, 17 Dec 2025 18:29:46 +0530 Subject: [PATCH 39/40] NMC 1984 - Privacy policy customisation added --- .../Data/NCManageDatabase+LocalFile.swift | 40 +- .../Data/NCManageDatabase+Metadata.swift | 66 + ...mon+CollectionViewDelegateFlowLayout.swift | 43 - .../NCCollectionViewCommon+SyncMetadata.swift | 14 +- .../NCCollectionViewCommon.swift | 1525 +++++------------ .../NCBackgroundLocationUploadManager.swift | 19 +- iOSClient/NCGlobal.swift | 22 +- iOSClient/Networking/NCConfigServer.swift | 25 +- .../Networking/NCNetworking+Download.swift | 62 +- .../Networking/NCNetworking+LivePhoto.swift | 25 +- iOSClient/Networking/NCNetworking+Task.swift | 25 +- .../Networking/NCNetworking+Upload.swift | 21 +- .../Networking/NCNetworking+WebDAV.swift | 69 +- iOSClient/Networking/NCNetworking.swift | 198 +++ .../Networking/NCNetworkingProcess.swift | 1 - iOSClient/Networking/NCService.swift | 25 +- .../en.lproj/Localizable.strings | 108 +- 17 files changed, 955 insertions(+), 1333 deletions(-) diff --git a/iOSClient/Data/NCManageDatabase+LocalFile.swift b/iOSClient/Data/NCManageDatabase+LocalFile.swift index 2146dec1ad..334e5fba25 100644 --- a/iOSClient/Data/NCManageDatabase+LocalFile.swift +++ b/iOSClient/Data/NCManageDatabase+LocalFile.swift @@ -29,14 +29,17 @@ extension NCManageDatabase { // MARK: - Realm Write - /// Adds or updates multiple local file entries corresponding to the given metadata array. - /// Uses async Realm read + single write transaction. Assumes `tableLocalFile` has a primary key. /// - Parameters: - /// - metadatas: Array of `tableMetadata` to map into `tableLocalFile`. - /// - offline: Optional override for the `offline` flag applied to all items. - func addLocalFilesAsync(metadatas: [tableMetadata], offline: Bool? = nil) async { - guard !metadatas.isEmpty else { - return + /// - metadata: The `tableMetadata` containing file details. + /// - offline: Optional flag to mark the file as available offline. + /// - Returns: Nothing. Realm write is performed asynchronously. + func addLocalFileAsync(metadata: tableMetadata, offline: Bool? = nil) async { + // Read (non-blocking): safely detach from Realm thread + let existing: tableLocalFile? = performRealmRead { realm in + realm.objects(tableLocalFile.self) + .filter(NSPredicate(format: "ocId == %@", metadata.ocId)) + .first + .map { tableLocalFile(value: $0) } } // Extract ocIds for efficient lookup @@ -59,20 +62,19 @@ extension NCManageDatabase { // Reuse existing object or create a new one let local = existingMap[metadata.ocId] ?? tableLocalFile() - local.account = metadata.account - local.etag = metadata.etag - local.exifDate = NSDate() - local.exifLatitude = "-1" - local.exifLongitude = "-1" - local.ocId = metadata.ocId - local.fileName = metadata.fileName + addObject.account = metadata.account + addObject.etag = metadata.etag + addObject.exifDate = NSDate() + addObject.exifLatitude = "-1" + addObject.exifLongitude = "-1" + addObject.ocId = metadata.ocId + addObject.fileName = metadata.fileName - if let offline { - local.offline = offline - } - - realm.add(local, update: .all) + if let offline { + addObject.offline = offline } + + realm.add(addObject, update: .all) } } diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index 67ed7ad2f2..5c3b840691 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -151,10 +151,37 @@ extension tableMetadata { (fileNameView as NSString).deletingPathExtension } + var isRenameable: Bool { + if !NCMetadataPermissions.canRename(self) { + return false + } + if lock { + return false + } + if !isDirectoryE2EE && e2eEncrypted { + return false + } + return true + } + + var isPrintable: Bool { + if isDocumentViewableOnly { + return false + } + if ["application/pdf", "com.adobe.pdf"].contains(contentType) || contentType.hasPrefix("text/") || classFile == NKTypeClassFile.image.rawValue { + return true + } + return false + } + var isSavebleInCameraRoll: Bool { return (classFile == NKTypeClassFile.image.rawValue && contentType != "image/svg+xml") || classFile == NKTypeClassFile.video.rawValue } + var isDocumentViewableOnly: Bool { + sharePermissionsCollaborationServices == NCPermissions().permissionReadShare && classFile == NKTypeClassFile.document.rawValue + } + var isAudioOrVideo: Bool { return classFile == NKTypeClassFile.audio.rawValue || classFile == NKTypeClassFile.video.rawValue } @@ -344,6 +371,17 @@ extension tableMetadata { return !lock || (lockOwner == user && lockOwnerType == 0) } + // Return if is sharable + func isSharable() -> Bool { + guard let capabilities = NCNetworking.shared.capabilities[account] else { + return false + } + if !capabilities.fileSharingApiEnabled || (capabilities.e2EEEnabled && isDirectoryE2EE), !e2eEncrypted { + return false + } + return !e2eEncrypted + } + /// Returns a detached (unmanaged) deep copy of the current `tableMetadata` object. /// /// - Note: Primitive properties and lists of primitive values (for example `shareType`) @@ -931,6 +969,34 @@ extension NCManageDatabase { .map { $0.detachedCopy() } } ?? [] } + + func getMediaMetadatas(predicate: NSPredicate, sorted: String? = nil, ascending: Bool = false) -> ThreadSafeArray? { + + do { + let realm = try Realm() + if let sorted { + var results: [tableMetadata] = [] + switch sorted {//NCPreferences().mediaSortDate { + case "date": + results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.date as Date) > ($1.date as Date) } + case "creationDate": + results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.creationDate as Date) > ($1.creationDate as Date) } + case "uploadDate": + results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.uploadDate as Date) > ($1.uploadDate as Date) } + default: + let results = realm.objects(tableMetadata.self).filter(predicate) + return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) + } + return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) + } else { + let results = realm.objects(tableMetadata.self).filter(predicate) + return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) + } + } catch let error as NSError { +// NextcloudKit.shared.nkCommonInstance.writeLog("Could not access database: \(error)") + } + return nil + } func getMetadatas(predicate: NSPredicate, sortedByKeyPath: String, diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift index 42bbbd6038..06795639bc 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift @@ -13,47 +13,4 @@ extension NCCollectionViewCommon: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { return sizeForFooterInSection(section: section) } - - func getHeaderHeight() -> CGFloat { - - var size: CGFloat = 0 - // transfer in progress - if headerMenuTransferView, - let metadata = NCManageDatabase.shared.getMetadataFromOcId(NCNetworking.shared.transferInForegorund?.ocId), - metadata.isTransferInForeground { - if !isSearchingMode { - size += NCGlobal.shared.heightHeaderTransfer - } - } else { - NCNetworking.shared.transferInForegorund = nil - } - - if headerMenuButtonsView { - size += NCGlobal.shared.heightButtonsView - } - - return size - } - - func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { - - var headerRichWorkspace: CGFloat = 0 - - if let richWorkspaceText = richWorkspaceText, showDescription { - let trimmed = richWorkspaceText.trimmingCharacters(in: .whitespaces) - if !trimmed.isEmpty && !isSearchingMode { - headerRichWorkspace = UIScreen.main.bounds.size.height / 6 - } - } - - if isSearchingMode || layoutForView?.layout == NCGlobal.shared.layoutGrid || dataSource.numberOfSections() > 1 { - if section == 0 { - return (getHeaderHeight(), headerRichWorkspace, NCGlobal.shared.heightSection) - } else { - return (0, 0, NCGlobal.shared.heightSection) - } - } else { - return (getHeaderHeight(), headerRichWorkspace, 0) - } - } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift index 5e0254858d..571306d2de 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift @@ -21,7 +21,7 @@ extension NCCollectionViewCommon { // If a sync task is already running, do not start a new one if let task = syncMetadatasTask, !task.isCancelled { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .info, message: "Exit: Another sync is already running. Skipping this one.", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .info, message: "Exit: Another sync is already running. Skipping this one.", consoleOnly: true) return } @@ -45,9 +45,9 @@ extension NCCollectionViewCommon { func stopSyncMetadata() async { if let task = syncMetadatasTask { if task.isCancelled { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .stop, message: "Sync Metadata for \(self.serverUrl) was already cancelled.", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .stop, message: "Sync Metadata for \(self.serverUrl) was already cancelled.", consoleOnly: true) } else { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .stop, message: "Stopping active Sync Metadata for \(self.serverUrl).", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .stop, message: "Stopping active Sync Metadata for \(self.serverUrl).", consoleOnly: true) } } @@ -74,7 +74,7 @@ extension NCCollectionViewCommon { return } let identifier = self.serverUrl + "_syncMetadata" - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .start, message: "Start Sync Metadata for \(self.serverUrl)") + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .start, message: "Start Sync Metadata for \(self.serverUrl)") // Always cancel and clear all tracked URLSessionTask on any exit path defer { @@ -85,7 +85,7 @@ extension NCCollectionViewCommon { // If a readFile for this serverUrl is already in-flight, do nothing if await networking.networkingTasks.isReading(identifier: identifier) { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .debug, message: "ReadFile for this \(self.serverUrl) is already in-flight.", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .debug, message: "ReadFile for this \(self.serverUrl) is already in-flight.", consoleOnly: true) return } @@ -151,9 +151,9 @@ extension NCCollectionViewCommon { // If this folder failed, skip it but keep processing others if resultsReadFolder.error == .success { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .network, message: "Read correctly: \(serverUrl)", consoleOnly: true) + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .network, message: "Read correctly: \(serverUrl)", consoleOnly: true) } else { - nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .error, message: "Read failed for \(serverUrl) with error: \(resultsReadFolder.error.errorDescription)") + nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .error, message: "Read failed for \(serverUrl) with error: \(resultsReadFolder.error.errorDescription)") return } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index f619b07794..9452410c87 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -11,7 +11,7 @@ import EasyTipView import LucidBanner import MoEngageInApps -class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionHeaderMenuDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate, NCEmptyDataSetDelegate { +class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionFirstHeaderDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { @IBOutlet weak var collectionView: UICollectionView! @@ -21,6 +21,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let utilityFileSystem = NCUtilityFileSystem() let imageCache = NCImageCache.shared var dataSource = NCCollectionViewDataSource() + let networking = NCNetworking.shared let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() @@ -58,12 +59,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // var headerMenu: NCSectionFirstHeader? var tipViewAccounts: EasyTipView? - var tipViewAutoUpload: EasyTipView? - var headerMenu: NCSectionHeaderMenu? - var headerMenuTransferView = false - var headerMenuButtonsView: Bool = true - var headerRichWorkspaceDisable: Bool = false - + var selectableDataSource: [RealmSwiftObject] { dataSource.getMetadataSourceForAllSections() } + // DECLARE var layoutKey = "" var titleCurrentFolder = "" @@ -77,98 +74,75 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var headerRichWorkspaceDisable: Bool = false var groupByField = "name" + var headerRichWorkspaceDisable: Bool = false - var emptyImage: UIImage? + var emptyImageName: String? + var emptyImageColors: [UIColor]? + var emptyTitle: String = "" - internal var emptyDescription: String = "" - internal var emptyDataPortaitOffset: CGFloat = 0 - internal var emptyDataLandscapeOffset: CGFloat = -20 + var emptyDescription: String = "" + var emptyDataPortaitOffset: CGFloat = 0 + var emptyDataLandscapeOffset: CGFloat = -20 - internal var lastScale: CGFloat = 1.0 - internal var currentScale: CGFloat = 1.0 - internal var maxColumns: Int { + var lastScale: CGFloat = 1.0 + var currentScale: CGFloat = 1.0 + var maxColumns: Int { let screenWidth = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) let column = Int(screenWidth / 44) return column } - internal var transitionColumns = false - internal var numberOfColumns: Int = 0 - internal var lastNumberOfColumns: Int = 0 + var transitionColumns = false + var numberOfColumns: Int = 0 + var lastNumberOfColumns: Int = 0 - var isTransitioning: Bool = false - var selectableDataSource: [RealmSwiftObject] { dataSource.getMetadataSourceForAllSections() } - var pushed: Bool = false - var emptyDataSet: NCEmptyDataSet? - let heightHeaderRecommendations: CGFloat = 160 let heightHeaderSection: CGFloat = 30 + @MainActor var session: NCSession.Session { NCSession.shared.getSession(controller: tabBarController) } - internal var isLayoutPhoto: Bool { + var isLayoutPhoto: Bool { layoutForView?.layout == global.layoutPhotoRatio || layoutForView?.layout == global.layoutPhotoSquare } - internal var isLayoutGrid: Bool { + var isLayoutGrid: Bool { layoutForView?.layout == global.layoutGrid } - internal var isLayoutList: Bool { + var isLayoutList: Bool { layoutForView?.layout == global.layoutList } - internal var showDescription: Bool { + var showDescription: Bool { !headerRichWorkspaceDisable && NCPreferences().showDescription } - internal var isRecommendationActived: Bool { + var isRecommendationActived: Bool { let capabilities = NCNetworking.shared.capabilities[session.account] ?? NKCapabilities.Capabilities() return self.serverUrl == self.utilityFileSystem.getHomeServer(session: self.session) && capabilities.recommendations } - internal var infoLabelsSeparator: String { + var infoLabelsSeparator: String { layoutForView?.layout == global.layoutList ? " - " : "" } @MainActor - internal var session: NCSession.Session { - NCSession.shared.getSession(controller: tabBarController) - } - - @MainActor - internal var controller: NCMainTabBarController? { + var controller: NCMainTabBarController? { self.tabBarController as? NCMainTabBarController } - @MainActor - internal var mainNavigationController: NCMainNavigationController? { + var mainNavigationController: NCMainNavigationController? { self.navigationController as? NCMainNavigationController } - @MainActor - internal var sceneIdentifier: String { + var sceneIdentifier: String { (self.tabBarController as? NCMainTabBarController)?.sceneIdentifier ?? "" } - var defaultPredicate: NSPredicate { - let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, NCGlobal.shared.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) - return predicate - } - - var personalFilesOnlyPredicate: NSPredicate { - let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND (ownerId == %@ || ownerId == '') AND mountType == '' AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, session.userId, global.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) - return predicate - } - - @MainActor - internal var windowScene: UIWindowScene? { - SceneManager.shared.getWindowScene(controller: self.tabBarController as? NCMainTabBarController) - } - - internal var isNumberOfItemsInAllSectionsNull: Bool { + var isNumberOfItemsInAllSectionsNull: Bool { var totalItems = 0 for section in 0.. Bool { - let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" - let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") - return currentVersion != appVersion + + removeImageCache(metadatas: self.dataSource.getMetadatas()) } - func redirectToPrivacyViewController() { - let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) - let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as! UINavigationController - newViewController.modalPresentationStyle = .fullScreen - self.present(newViewController, animated: true, completion: nil) + func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { + let viewController = presentationController.presentedViewController + + if viewController is NCViewerRichWorkspaceWebView { + closeRichWorkspaceWebView() + } } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) coordinator.animate(alongsideTransition: { _ in - self.collectionView?.collectionViewLayout.invalidateLayout() + let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) { + self.collectionView?.collectionViewLayout.invalidateLayout() + } + animator.startAnimation() }) self.dismissTip() @@ -520,12 +381,61 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS override var canBecomeFirstResponder: Bool { return true } + + @objc func updateIcons() { + collectionView.reloadData() + } - func presentationControllerDidDismiss( _ presentationController: UIPresentationController) { - let viewController = presentationController.presentedViewController - - if viewController is NCViewerRichWorkspaceWebView { - closeRichWorkspaceWebView() + func isApplicationUpdated() -> Bool { + let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String ?? "" + let currentVersion = UserDefaults.standard.string(forKey: "CurrentAppVersion") + return currentVersion != appVersion + } + + func redirectToPrivacyViewController() { + let storyBoard: UIStoryboard = UIStoryboard(name: "NCSettings", bundle: nil) + let newViewController = storyBoard.instantiateViewController(withIdentifier: "privacySettingsNavigation") as? UINavigationController + newViewController?.modalPresentationStyle = .fullScreen + self.present(newViewController!, animated: true, completion: nil) + } + + // MARK: - Transfer Delegate + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + + func transferChange(status: String, metadatasError: [tableMetadata: NKError]) { + switch status { + // DELETE + case self.global.networkingStatusDelete: + let errorForThisServer = metadatasError.first { entry in + let (key, value) = entry + return key.serverUrl == self.serverUrl && value != .success + }?.value + + let needLoadDataSource = metadatasError.contains { entry in + let (key, value) = entry + return key.serverUrl == self.serverUrl && value == .success + } + + if let error = errorForThisServer { + NCContentPresenter().showError(error: error) + } + + if self.isSearchingMode { + self.networkSearch() + } else if needLoadDataSource { + Task { + await self.reloadDataSource() + } + } else { + Task.detached { + if await self.isRecommendationActived() { + await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) + } + } + } + default: + break } } @@ -873,719 +783,102 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } @objc func closeRichWorkspaceWebView() { - reloadDataSource() + Task { + await self.reloadDataSource() + } } - @objc func deleteFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let error = userInfo["error"] as? NKError else { return } + // MARK: - Layout - if error == .success { - if isSearchingMode { - return networkSearch() - } + func changeLayout(layoutForView: NCDBLayoutForView) { + let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) + let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) + func changeLayout(withSubFolders: Bool) { + if self.layoutForView?.layout == layoutForView.layout { + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + Task { + await self.reloadDataSource() } + return } - } else { - NCContentPresenter().showError(error: error) - } - - reloadDataSource() - } - @objc func copyMoveFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - account == session.account else { return } + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView, withSubFolders: withSubFolders) + layoutForView.layout = layoutForView.layout + self.layoutType = layoutForView.layout - if isSearchingMode { - return networkSearch() - } + collectionView.reloadData() - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break } - } - if serverUrl == self.serverUrl { - reloadDataSource() - } + self.collectionView.collectionViewLayout.invalidateLayout() - @objc func reloadAvatar(_ notification: NSNotification) { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.showTipAccounts() + Task { + await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + } } - guard let userInfo = notification.userInfo as NSDictionary?, - let error = userInfo["error"] as? NKError, - error.errorCode != global.errorNotModified else { return } - /// Magentacloud branding changes hide user account button on left navigation bar - setNavigationLeftItems() - } - @objc func renameFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let account = userInfo["account"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let error = userInfo["error"] as? NKError, - account == session.account - else { return } + if serverUrl == homeServer || numFoldersLayoutsForView == 1 { + changeLayout(withSubFolders: false) + } else { + let alertController = UIAlertController(title: NSLocalizedString("_propagate_layout_", comment: ""), message: nil, preferredStyle: .alert) - if error == .success { - if isSearchingMode { - return networkSearch() - } + alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default, handler: { _ in + changeLayout(withSubFolders: true) + })) + alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in + changeLayout(withSubFolders: false) + })) - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } + self.present(alertController, animated: true) } + } - if serverUrl == self.serverUrl { - if error != .success { - NCContentPresenter().showError(error: error) - } - reloadDataSource() - } else { - collectionView.reloadData() + func getNavigationTitle() -> String { + let tblAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) + if let tblAccount, + !tblAccount.alias.isEmpty { + return tblAccount.alias } + return NCBrandOptions.shared.brand } - @objc func createFolder(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let account = userInfo["account"] as? String, - account == session.account, - let withPush = userInfo["withPush"] as? Bool, - let metadata = database.getMetadataFromOcId(ocId) - else { return } + func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } - if isSearchingMode { - return networkSearch() - } + @MainActor + func startGUIGetServerData() { + self.dataSource.setGetServerData(false) + self.collectionView.reloadData() - if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { - reloadDataSource() - } else if withPush, metadata.serverUrl == self.serverUrl { - reloadDataSource() - if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { - if sceneIdentifier == controller?.sceneIdentifier { - pushMetadata(metadata) - } - } else { - pushMetadata(metadata) - } + // Don't show spinner on iPad root folder + if UIDevice.current.userInterfaceIdiom == .pad, + (self.serverUrl == self.utilityFileSystem.getHomeServer(session: self.session)) || self.serverUrl.isEmpty { + return } - } - @objc func favoriteFile(_ notification: NSNotification) { - if isSearchingMode { - return networkSearch() - } + let spinner = UIActivityIndicatorView(style: .medium) + spinner.startAnimating() - if self is NCFavorite { - return reloadDataSource() - } + let container = UIView() + container.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(spinner) - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - serverUrl == self.serverUrl - else { return } + spinner.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + spinner.centerXAnchor.constraint(equalTo: container.centerXAnchor), + spinner.centerYAnchor.constraint(equalTo: container.centerYAnchor) + ]) - reloadDataSource() - } - - @objc func downloadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func downloadedFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func downloadCancelFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - !isSearchingMode, - let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) - else { return } - - // Header view trasfer - if metadata.isTransferInForeground { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) - DispatchQueue.main.async { self.collectionView?.reloadData() } - } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadedFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadedLivePhoto(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadCancelFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func triggerProgressTask(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let progressNumber = userInfo["progress"] as? NSNumber, - let totalBytes = userInfo["totalBytes"] as? Int64, - let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, - let ocId = userInfo["ocId"] as? String, - let ocIdTransfer = userInfo["ocIdTransfer"] as? String, - let session = userInfo["session"] as? String - else { return } - - let chunk: Int = userInfo["chunk"] as? Int ?? 0 - let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false - - let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) - -// DispatchQueue.main.async { - if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { - if NCNetworking.shared.transferInForegorund?.ocId == ocId { - NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue - } else { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) - self.collectionView.reloadData() - } - self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue - } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId), - let cell = self.collectionView?.cellForItem(at: indexPath), - let cell = cell as? NCCellProtocol else { return } - if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { - cell.fileProgressView?.isHidden = true - cell.fileProgressView?.progress = .zero - cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) - if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { - cell.writeInfoDateSize(date: metadata.date, size: metadata.size) - } else { - cell.fileInfoLabel?.text = "" - cell.fileSubinfoLabel?.text = "" - } - } else { - cell.fileProgressView?.isHidden = false - cell.fileProgressView?.progress = progressNumber.floatValue - cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) - let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal - if status == NCGlobal.shared.metadataStatusDownloading { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) - } else if status == NCGlobal.shared.metadataStatusUploading { - if totalBytes > 0 { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) - } else { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" - } - } - } - } -// } - } - - @objc func updateShare(_ notification: NSNotification) { - if isSearchingMode { - networkSearch() - } else { -// self.dataSource.removeAll() - getServerData() - } - } - - @objc func updateIcons() { - collectionView.reloadData() -// reloadDataSource() - } - - // MARK: - Layout - - func changeLayout(layoutForView: NCDBLayoutForView) { - let homeServer = utilityFileSystem.getHomeServer(urlBase: session.urlBase, userId: session.userId) - let numFoldersLayoutsForView = self.database.getLayoutsForView(keyStore: layoutForView.keyStore)?.count ?? 1 - - - if self.layoutForView?.layout == layoutForView.layout { - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) - self.reloadDataSource() - return - } - - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) - layoutForView.layout = layoutForView.layout - self.layoutType = layoutForView.layout - - collectionView.reloadData() - - switch layoutForView.layout { - case global.layoutList: - self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) - case global.layoutGrid: - self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) - case global.layoutPhotoSquare, global.layoutPhotoRatio: - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) - default: - break - } - - self.collectionView.collectionViewLayout.invalidateLayout() - -// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() - } - - @objc func reloadDataSource(_ notification: NSNotification) { - if let userInfo = notification.userInfo as? NSDictionary { - if let serverUrl = userInfo["serverUrl"] as? String { - if serverUrl != self.serverUrl { - return - } - } - - if let clearDataSource = userInfo["clearDataSource"] as? Bool, clearDataSource { - self.dataSource.removeAll() - } - } - - reloadDataSource() - } - - @objc func getServerData(_ notification: NSNotification) { - if let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String { - if serverUrl != self.serverUrl { - return - } - } - - getServerData() - } - - @objc func reloadHeader(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let account = userInfo["account"] as? String, - account == session.account - else { return } - - self.collectionView.reloadData() - } - - @objc func changeStatusFolderE2EE(_ notification: NSNotification) { - reloadDataSource() - } - - @objc func closeRichWorkspaceWebView() { - reloadDataSource() - } - - @objc func deleteFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let error = userInfo["error"] as? NKError else { return } - - if error == .success { - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - } else { - NCContentPresenter().showError(error: error) - } - - reloadDataSource() - } - - @objc func copyMoveFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - account == session.account else { return } - - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - - if serverUrl == self.serverUrl { - reloadDataSource() - } - } - - @objc func renameFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let account = userInfo["account"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let error = userInfo["error"] as? NKError, - account == session.account - else { return } - - if error == .success { - if isSearchingMode { - return networkSearch() - } - - if isRecommendationActived { - Task.detached { - await NCNetworking.shared.createRecommendations(session: self.session) - } - } - } - - if serverUrl == self.serverUrl { - if error != .success { - NCContentPresenter().showError(error: error) - } - reloadDataSource() - } else { - collectionView.reloadData() - } - } - - @objc func createFolder(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let account = userInfo["account"] as? String, - account == session.account, - let withPush = userInfo["withPush"] as? Bool, - let metadata = database.getMetadataFromOcId(ocId) - else { return } - - if isSearchingMode { - return networkSearch() - } - - if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { - reloadDataSource() - } else if withPush, metadata.serverUrl == self.serverUrl { - reloadDataSource() - if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { - if sceneIdentifier == controller?.sceneIdentifier { - pushMetadata(metadata) - } - } else { - pushMetadata(metadata) - } - } - } - - @objc func favoriteFile(_ notification: NSNotification) { - if isSearchingMode { - return networkSearch() - } - - if self is NCFavorite { - return reloadDataSource() - } - - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - serverUrl == self.serverUrl - else { return } - - reloadDataSource() - } - - @objc func downloadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func downloadedFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func downloadCancelFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - !isSearchingMode, - let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) - else { return } - - // Header view trasfer - if metadata.isTransferInForeground { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) - DispatchQueue.main.async { self.collectionView?.reloadData() } - } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadedFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadedLivePhoto(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func uploadCancelFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String - else { return } - - if account == self.session.account, serverUrl == self.serverUrl { - reloadDataSource() - } else { - collectionView?.reloadData() - } - } - - @objc func triggerProgressTask(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let progressNumber = userInfo["progress"] as? NSNumber, - let totalBytes = userInfo["totalBytes"] as? Int64, - let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, - let ocId = userInfo["ocId"] as? String, - let ocIdTransfer = userInfo["ocIdTransfer"] as? String, - let session = userInfo["session"] as? String - else { return } - - let chunk: Int = userInfo["chunk"] as? Int ?? 0 - let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false - - let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) - -// DispatchQueue.main.async { - if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { - if NCNetworking.shared.transferInForegorund?.ocId == ocId { - NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue - } else { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) - self.collectionView.reloadData() - } - self.headerMenu?.progressTransfer.progress = transfer.progressNumber.floatValue - } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId), - let cell = self.collectionView?.cellForItem(at: indexPath), - let cell = cell as? NCCellProtocol else { return } - if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { - cell.fileProgressView?.isHidden = true - cell.fileProgressView?.progress = .zero - cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) - if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { - cell.writeInfoDateSize(date: metadata.date, size: metadata.size) - } else { - cell.fileInfoLabel?.text = "" - cell.fileSubinfoLabel?.text = "" - } - } else { - cell.fileProgressView?.isHidden = false - cell.fileProgressView?.progress = progressNumber.floatValue - cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) - let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal - if status == NCGlobal.shared.metadataStatusDownloading { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) - } else if status == NCGlobal.shared.metadataStatusUploading { - if totalBytes > 0 { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) - } else { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" - } - } - } - } -// } - } - - @objc func updateShare(_ notification: NSNotification) { - if isSearchingMode { - networkSearch() - } else { -// self.dataSource.removeAll() - getServerData() - } - } - - @objc func updateIcons() { - collectionView.reloadData() -// reloadDataSource() - } - - // MARK: - Layout - - func setNavigationLeftItems() { - navigationItem.title = titleCurrentFolder - } - - func getNavigationTitle() -> String { - let tableAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) - if let tableAccount, - !tableAccount.alias.isEmpty { - return tableAccount.alias - } - return NCBrandOptions.shared.brand - } - - func accountSettingsDidDismiss(tableAccount: tableAccount?, controller: NCMainTabBarController?) { } - - @MainActor - func startGUIGetServerData() { - self.dataSource.setGetServerData(false) - - // Don't show spinner on iPad root folder - if UIDevice.current.userInterfaceIdiom == .pad, - (self.serverUrl == self.utilityFileSystem.getHomeServer(session: self.session)) || self.serverUrl.isEmpty { - return - } - - let spinner = UIActivityIndicatorView(style: .medium) - spinner.startAnimating() - - let container = UIView() - container.translatesAutoresizingMaskIntoConstraints = false - container.addSubview(spinner) - - spinner.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - spinner.centerXAnchor.constraint(equalTo: container.centerXAnchor), - spinner.centerYAnchor.constraint(equalTo: container.centerYAnchor) - ]) - - self.navigationItem.titleView = container + self.navigationItem.titleView = container } @MainActor @@ -1627,83 +920,90 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS view.emptyDescription.text = NSLocalizedString("_no_file_pull_down_", comment: "") } } + + let spinner = UIActivityIndicatorView(style: .medium) + spinner.startAnimating() + + let container = UIView() + container.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(spinner) + + spinner.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + spinner.centerXAnchor.constraint(equalTo: container.centerXAnchor), + spinner.centerYAnchor.constraint(equalTo: container.centerYAnchor) + ]) + + self.navigationItem.titleView = container } - + + @MainActor + func restoreDefaultTitle() { + self.navigationItem.titleView = nil + self.navigationItem.title = self.titleCurrentFolder + } + // MARK: - SEARCH func searchController(enabled: Bool) { - guard enableSearchBar else { - return - } + guard enableSearchBar else { return } searchController?.searchBar.isUserInteractionEnabled = enabled - if enabled { searchController?.searchBar.alpha = 1 } else { searchController?.searchBar.alpha = 0.3 + } } func updateSearchResults(for searchController: UISearchController) { - searchResultText = searchController.searchBar.text + self.literalSearch = searchController.searchBar.text } func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + isSearchingMode = true + self.providers?.removeAll() + self.dataSource.removeAll() + Task { + await self.reloadDataSource() + } // TIP dismissTip() - - // (+) - self.mainNavigationController?.menuPlus?.hiddenPlusButton(true) - - if !isSearchingMode { - self.isSearchingMode = true - self.dataSource.removeAll() - self.collectionView.reloadData() - } -// isSearchingMode = true -// self.providers?.removeAll() -// self.dataSource.removeAll() -// self.reloadDataSource() -// // TIP -// dismissTip() + // + mainNavigationController?.hiddenPlusButton(true) } func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { - if isSearchingMode, - searchResultText?.count ?? 0 >= 2 { - Task { - await self.search() - } -// if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { -// networkSearch() + if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { + networkSearch() } } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - NCNetworking.shared.cancelUnifiedSearchFiles() + self.networking.cancelUnifiedSearchFiles() + self.isSearchingMode = false self.literalSearch = "" self.providers?.removeAll() self.dataSource.removeAll() - self.reloadDataSource() + Task { + await self.reloadDataSource() + } + // + mainNavigationController?.hiddenPlusButton(false) } // MARK: - TAP EVENT - func tapMoreListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { - tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) - } - func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) } - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { - tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, namedButtonMore: namedButtonMore, image: image, sender: sender) + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) } func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) { - if isEditMode { return } guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) @@ -1712,29 +1012,11 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS appDelegate.adjust.trackEvent(TriggerEvent(Sharing.rawValue)) } - func tapMoreGridItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, image: UIImage?, sender: Any) { - if isEditMode { return } - guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } -// toggleMenu(metadata: metadata, image: image) - if namedButtonMore == NCGlobal.shared.buttonMoreMore || namedButtonMore == NCGlobal.shared.buttonMoreLock { - toggleMenu(metadata: metadata, image: image) - } else if namedButtonMore == NCGlobal.shared.buttonMoreStop { - Task { - await cancelSession(metadata: metadata) - } - } - } - func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } -// toggleMenu(metadata: metadata, image: image) - Task { - await cancelSession(metadata: metadata) - } + toggleMenu(metadata: metadata, image: image, sender: sender) } - func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func tapRichWorkspace(_ sender: Any) { if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { @@ -1748,88 +1030,21 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } - func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - // (+) - self.mainNavigationController?.menuPlus?.hiddenPlusButton(false) - - self.isSearchingMode = false - self.networkSearchInProgress = false - self.searchResultText = nil - self.searchResultStore = nil - - Task { - await searchOperationHandle.cancel() - await reloadDataSource() - - // Restore Layout - if let layoutForViewLayoutStore { - let layoutForView = database.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) - layoutForView.layout = layoutForViewLayoutStore - await setLayout(layoutForView: layoutForView) - } - layoutForViewLayoutStore = nil - - // update Option menu - await mainNavigationController?.updateMenuOption() - } - } - - func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?) { - toggleMenu(metadata: metadata, image: image) + func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { + toggleMenu(metadata: metadata, image: image, sender: sender) } - @MainActor - func setSearchBarLoading(_ loading: Bool) { - guard let textField = searchController?.searchBar.searchTextField else { - return - } - if loading { - let spinner = UIActivityIndicatorView(style: .medium) - spinner.startAnimating() - textField.rightView = spinner - textField.rightViewMode = .always - } else { - textField.rightView = nil - } + func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { + unifiedSearchMore(metadataForSection: metadataForSection) } - // MARK: - TAP EVENT - func tapRecommendations(with metadata: tableMetadata) { didSelectMetadata(metadata, withOcIds: false) } - func tapButtonSwitch(_ sender: Any) { - guard !isTransitioning else { return } - isTransitioning = true - - guard let layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) else { return } - - if layoutForView.layout == NCGlobal.shared.layoutGrid { - layoutForView.layout = NCGlobal.shared.layoutList - } else { - layoutForView.layout = NCGlobal.shared.layoutGrid - } - self.layoutForView = NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) - self.collectionView.reloadData() - self.collectionView.collectionViewLayout.invalidateLayout() - self.collectionView.setCollectionViewLayout(layoutForView.layout == NCGlobal.shared.layoutList ? self.listLayout : self.gridLayout, animated: true) {_ in self.isTransitioning = false } - } - - func tapButtonOrder(_ sender: Any) { - let sortMenu = NCSortMenu() - sortMenu.toggleMenu(viewController: self, account: session.account, key: layoutKey, sortButton: sender as? UIButton, serverUrl: serverUrl) - } - - func tapButtonTransfer(_ sender: Any) { } - - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } - func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func longPressListItem(with objectId: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) { - } - func longPressListItem(with ocId: String, ocIdTransfer: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func longPressGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } func longPressMoreGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } @@ -1868,7 +1083,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS becomeFirstResponder() if !serverUrl.isEmpty { - listMenuItems.append(UIMenuItem(title: NSLocalizedString("_paste_file_", comment: ""), action: #selector(pasteFilesMenu))) + listMenuItems.append(UIMenuItem(title: NSLocalizedString("_paste_file_", comment: ""), action: #selector(pasteFilesMenu(_:)))) } if !listMenuItems.isEmpty { @@ -1877,23 +1092,16 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } - // MARK: - Transfer Delegate - - func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } - - func tranferChange(status: String, metadata: tableMetadata, error: NKError) { } - // MARK: - Menu Item override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - - if #selector(pasteFilesMenu) == action { + if #selector(pasteFilesMenu(_:)) == action { if !UIPasteboard.general.items.isEmpty, !(metadataFolder?.e2eEncrypted ?? false) { return true } - } else if #selector(copyMenuFile) == action { + } else if #selector(copyMenuFile(_:)) == action { return true - } else if #selector(moveMenuFile) == action { + } else if #selector(moveMenuFile(_:)) == action { return true } @@ -1996,95 +1204,168 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) } - await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: self.serverUrl) + DispatchQueue.main.async { +// UIView.transition(with: self.collectionView, +// duration: 0.20, +// options: .transitionCrossDissolve, +// animations: { self.collectionView.reloadData() }, +// completion: nil) +// +// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() + if isRecommendationActived { + Task.detached { + await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) + } + } } - await mainNavigationController?.updateMenuOption() - (self.navigationController as? NCMainNavigationController)?.updateRightMenu() + DispatchQueue.main.async { +// UIView.transition(with: self.collectionView, +// duration: 0.20, +// options: .transitionCrossDissolve, +// animations: { self.collectionView.reloadData() }, +// completion: nil) +// +// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() self.refreshControl.endRefreshing() self.collectionView.reloadData() self.setNavigationRightItems() } } - func getServerData() { - } - - // MARK: - Push metadata + func getServerData(forced: Bool = false) async { } - /// Pushes or reuses the folder view controller associated with the provided metadata. - /// - /// - Parameter metadata: The metadata representing the selected folder. - @MainActor - func pushMetadata(_ metadata: tableMetadata) async { - guard !isNavigatingMetadata, - let navigationController = self.navigationController, - let navigationCollectionViewCommon = self.controller?.navigationCollectionViewCommon else { + @objc func networkSearch() { + guard !networkSearchInProgress else { return } - - isNavigatingMetadata = true - defer { - isNavigatingMetadata = false + guard !session.account.isEmpty, + let literalSearch = literalSearch, + !literalSearch.isEmpty else { + return } + let capabilities = NCNetworking.shared.capabilities[session.account] ?? NKCapabilities.Capabilities() - let serverUrlPush = utilityFileSystem.createServerUrl( - serverUrl: metadata.serverUrl, - fileName: metadata.fileName - ) - - // Update the last opening date without blocking the main flow. - Task.detached(priority: .utility) { [database] in - await database.setDirectoryLastOpeningDateAsync(ocId: metadata.ocId) + self.networkSearchInProgress = true + self.dataSource.removeAll() + Task { + await self.reloadDataSource() } - guard navigationController.transitionCoordinator == nil else { - return + if capabilities.serverVersionMajor >= global.nextcloudVersion20 { + self.networking.unifiedSearchFiles(literal: literalSearch, account: session.account) { task in + self.searchDataSourceTask = task + Task { + await self.reloadDataSource() + } + } providers: { account, searchProviders in + self.providers = searchProviders + self.searchResults = [] + self.dataSource = NCCollectionViewDataSource(metadatas: [], + layoutForView: self.layoutForView, + providers: self.providers, + searchResults: self.searchResults, + account: account) + } update: { _, _, searchResult, metadatas in + guard let metadatas, !metadatas.isEmpty, self.isSearchingMode, let searchResult else { return } + self.networking.unifiedSearchQueue.addOperation(NCCollectionViewUnifiedSearch(collectionViewCommon: self, metadatas: metadatas, searchResult: searchResult)) + } completion: { _, _ in + Task { + await self.reloadDataSource() + } + self.networkSearchInProgress = false + } + } else { + self.networking.searchFiles(literal: literalSearch, account: session.account) { task in + self.searchDataSourceTask = task + Task { + await self.reloadDataSource() + } + } completion: { metadatasSearch, error in + Task { + guard let metadatasSearch, + error == .success, + self.isSearchingMode + else { + self.networkSearchInProgress = false + await self.reloadDataSource() + return + } + let ocId = metadatasSearch.map { $0.ocId } + let metadatas = await self.database.getMetadatasAsync(predicate: NSPredicate(format: "ocId IN %@", ocId), + withLayout: self.layoutForView, + withAccount: self.session.account) + + self.dataSource = NCCollectionViewDataSource(metadatas: metadatas, + layoutForView: self.layoutForView, + providers: self.providers, + searchResults: self.searchResults, + account: self.session.account) + self.networkSearchInProgress = false + await self.reloadDataSource() + } + } } + } - if let existingEntry = navigationCollectionViewCommon.first(where: { - $0.navigationController === navigationController && $0.serverUrl == serverUrlPush - }) { - let viewController = existingEntry.viewController + func unifiedSearchMore(metadataForSection: NCMetadataForSection?) { + guard let metadataForSection = metadataForSection, let lastSearchResult = metadataForSection.lastSearchResult, let cursor = lastSearchResult.cursor, let term = literalSearch else { return } - if navigationController.topViewController === viewController { - return - } + metadataForSection.unifiedSearchInProgress = true + self.collectionView?.reloadData() - if navigationController.viewControllers.contains(where: { $0 === viewController }) { - navigationController.popToViewController(viewController, animated: true) - return + self.networking.unifiedSearchFilesProvider(id: lastSearchResult.id, term: term, limit: 5, cursor: cursor, account: session.account) { task in + self.searchDataSourceTask = task + Task { + await self.reloadDataSource() + } + } completion: { _, searchResult, metadatas, error in + if error != .success { + Task {@MainActor in + await showErrorBanner( + controller: self.controller, + errorDescription: error.errorDescription, + errorCode: error.errorCode + ) + } } - navigationController.pushViewController(viewController, animated: true) - return + metadataForSection.unifiedSearchInProgress = false + guard let searchResult = searchResult, let metadatas = metadatas else { return } + self.dataSource.appendMetadatasToSection(metadatas, metadataForSection: metadataForSection, lastSearchResult: searchResult) + + DispatchQueue.main.async { + self.collectionView?.reloadData() + } } + } + + // MARK: - Push metadata - guard let viewController = UIStoryboard(name: "NCFiles", bundle: nil).instantiateInitialViewController() as? NCFiles else { + func pushMetadata(_ metadata: tableMetadata) { + guard let navigationCollectionViewCommon = self.controller?.navigationCollectionViewCommon else { return } + let serverUrlPush = utilityFileSystem.createServerUrl(serverUrl: metadata.serverUrl, fileName: metadata.fileName) - viewController.serverUrl = serverUrlPush - viewController.titlePreviusFolder = navigationItem.title - viewController.titleCurrentFolder = metadata.fileNameView + // Set Last Opening Date + Task { + await database.setDirectoryLastOpeningDateAsync(ocId: metadata.ocId) + } - navigationCollectionViewCommon.append( - NavigationCollectionViewCommon( - serverUrl: serverUrlPush, - navigationController: navigationController, - viewController: viewController - ) - ) + if let viewController = navigationCollectionViewCommon.first(where: { $0.navigationController == self.navigationController && $0.serverUrl == serverUrlPush})?.viewController, viewController.isViewLoaded { + navigationController?.pushViewController(viewController, animated: true) + } else { + if let viewController: NCFiles = UIStoryboard(name: "NCFiles", bundle: nil).instantiateInitialViewController() as? NCFiles { + viewController.serverUrl = serverUrlPush + viewController.titlePreviusFolder = navigationItem.title + viewController.titleCurrentFolder = metadata.fileNameView - navigationController.pushViewController(viewController, animated: true) - } - - func pushViewController(viewController: UIViewController) { - if pushed { return } + navigationCollectionViewCommon.append(NavigationCollectionViewCommon(serverUrl: serverUrlPush, navigationController: self.navigationController, viewController: viewController)) - pushed = true - navigationController?.pushViewController(viewController, animated: true) + navigationController?.pushViewController(viewController, animated: true) + } + } } func pushViewController(viewController: UIViewController) { @@ -2103,7 +1384,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS !results.isEmpty { return Array(results) } - return nil } func sizeForHeaderInSection(section: Int) -> CGSize { @@ -2112,12 +1392,12 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let isIphone = UIDevice.current.userInterfaceIdiom == .phone if self.dataSource.isEmpty() { - height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset, isHeaderMenuTransferViewEnabled: isHeaderMenuTransferViewEnabled() != nil) + height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset) } else if isEditMode || (isLandscape && isIphone) { return CGSize.zero } else { - let (heightHeaderCommands, heightHeaderRichWorkspace, heightHeaderSection) = getHeaderHeight(section: section) - height = heightHeaderCommands + heightHeaderRichWorkspace + heightHeaderSection + let (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) = getHeaderHeight(section: section) + height = heightHeaderRichWorkspace + heightHeaderRecommendations + heightHeaderSection } return CGSize(width: collectionView.frame.width, height: height) @@ -2126,14 +1406,23 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // MARK: - Footer size func sizeForFooterInSection(section: Int) -> CGSize { + guard let controller else { + return CGSize.zero + } let sections = dataSource.numberOfSections() - let metadataForSection = self.dataSource.getMetadataForSection(section) - let isPaginated = metadataForSection?.lastSearchResult?.isPaginated ?? false - let metadatasCount: Int = metadataForSection?.lastSearchResult?.entries.count ?? 0 - var size = CGSize(width: collectionView.frame.width, height: 0) + let bottomAreaInsets: CGFloat = controller.tabBar.safeAreaInsets.bottom == 0 ? 34 : 0 + let height = controller.tabBar.frame.height + bottomAreaInsets + + if isEditMode { + return CGSize(width: collectionView.frame.width, height: 90 + height) + } + + if isSearchingMode { + return CGSize(width: collectionView.frame.width, height: 50) + } if section == sections - 1 { - size.height += 85 + return CGSize(width: collectionView.frame.width, height: height) } else { size.height += 1 } diff --git a/iOSClient/NCBackgroundLocationUploadManager.swift b/iOSClient/NCBackgroundLocationUploadManager.swift index 154fda662d..8f63b82e9d 100644 --- a/iOSClient/NCBackgroundLocationUploadManager.swift +++ b/iOSClient/NCBackgroundLocationUploadManager.swift @@ -1,6 +1,10 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2025 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCBackgroundLocationUploadManager.swift +// Nextcloud +// +// Created by Marino Faggiana on 06/06/25. +// Copyright © 2025 Marino Faggiana. All rights reserved. +// import CoreLocation import NextcloudKit @@ -102,16 +106,15 @@ class NCBackgroundLocationUploadManager: NSObject, CLLocationManagerDelegate { } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - // Must work only in background guard isAppInBackground else { return } // Open Realm - guard NCManageDatabase.shared.openRealmBackground() else { - nkLog(tag: self.global.logTagLocation, emoji: .error, message: "Failed to open Realm in Location Manager") - return - } + if database.openRealmBackground() { + let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! + let location = locations.last + nkLog(tag: self.global.logTagLocation, emoji: .start, message: "Triggered by location change: \(location?.coordinate.latitude ?? 0), \(location?.coordinate.longitude ?? 0)") let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! let location = locations.last diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index afa29afca6..c7c11d0a81 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -50,7 +50,7 @@ final class NCGlobal: Sendable { // let avatarSize: Int = 128 * Int(UIScreen.main.scale) let avatarSizeRounded: Int = 128 - + // Preview size // let size1024: CGSize = CGSize(width: 1024, height: 1024) @@ -104,7 +104,6 @@ final class NCGlobal: Sendable { let layoutViewOffline = "LayoutOffline" let layoutViewFavorite = "LayoutFavorite" let layoutViewFiles = "LayoutFiles" - let layoutViewTransfers = "LayoutTransfers" let layoutViewRecent = "LayoutRecent" let layoutViewShares = "LayoutShares" let layoutViewShareExtension = "LayoutShareExtension" @@ -126,6 +125,7 @@ final class NCGlobal: Sendable { let heightFooterButton: CGFloat = 30 let endHeightFooter: CGFloat = 85 + // Text - OnlyOffice - Collabora - QuickLook // let editorText = "text" @@ -259,7 +259,6 @@ final class NCGlobal: Sendable { let metadataStatusForScreenAwake = [-1, -2, 1, 2] let metadataStatusHideInView = [1, 2, 3, 11] let metadataStatusWaitWebDav = [10, 11, 12, 13, 14, 15] - let metadataStatusTransfers = [-2, -3, 2, 3, 10, 11, 12, 13, 14, 15] let metadatasStatusInWaiting = [-1, 1, 10, 11, 12, 13, 14, 15] let metadatasStatusInProgress = [-2, 2] @@ -276,22 +275,16 @@ final class NCGlobal: Sendable { let notificationCenterChangeTheming = "changeTheming" // userInfo: account let notificationCenterRichdocumentGrabFocus = "richdocumentGrabFocus" let notificationCenterReloadDataNCShare = "reloadDataNCShare" + let notificationCenterDidCreateShareLink = "didCreateShareLink" + let notificationCenterCloseRichWorkspaceWebView = "closeRichWorkspaceWebView" let notificationCenterReloadAvatar = "reloadAvatar" let notificationCenterClearCache = "clearCache" let notificationCenterCheckUserDelaultErrorDone = "checkUserDelaultErrorDone" // userInfo: account, controller let notificationCenterServerDidUpdate = "serverDidUpdate" // userInfo: account let notificationCenterNetworkReachability = "networkReachability" - let notificationCenterDidCreateShareLink = "didCreateShareLink" - let notificationCenterDeleteFile = "deleteFile" // userInfo: [ocId], error - let notificationCenterCopyMoveFile = "copyMoveFile" // userInfo: [ocId] serverUrl, account, dragdrop, type (copy, move) - let notificationCenterMoveFile = "moveFile" // userInfo: [ocId], [indexPath], error - let notificationCenterCopyFile = "copyFile" // userInfo: [ocId], [indexPath], error let notificationCenterRenameFile = "renameFile" // userInfo: serverUrl, account, error - let notificationCenterFavoriteFile = "favoriteFile" // userInfo: ocId, serverUrl - let notificationCenterFileExists = "fileExists" // userInfo: ocId, fileExists - let notificationCenterReloadDataSource = "reloadDataSource" // userInfo: serverUrl?, clearDataSource let notificationCenterMenuSearchTextPDF = "menuSearchTextPDF" let notificationCenterMenuGotToPageInPDF = "menuGotToPageInPDF" @@ -299,7 +292,6 @@ final class NCGlobal: Sendable { let notificationCenterOpenMediaDetail = "openMediaDetail" // userInfo: ocId let notificationCenterDismissScanDocument = "dismissScanDocument" - let notificationCenterDismissUploadAssets = "dismissUploadAssets" let notificationCenterEnableSwipeGesture = "enableSwipeGesture" let notificationCenterDisableSwipeGesture = "disableSwipeGesture" @@ -424,7 +416,7 @@ final class NCGlobal: Sendable { let keyFileNameAutoUploadType = "fileNameAutoUploadType" let keyFileNameOriginal = "fileNameOriginal" let keyFileNameOriginalAutoUpload = "fileNameOriginalAutoUpload" - + // LOG TAG // let logTagTask = "BGT" @@ -443,10 +435,6 @@ final class NCGlobal: Sendable { // let udMigrationMultiDomains = "migrationMultiDomains" let udLastVersion = "lastVersion" - - // Album - // - let selectedTabIndexAlbum: Int = 3 } /** diff --git a/iOSClient/Networking/NCConfigServer.swift b/iOSClient/Networking/NCConfigServer.swift index f82b466647..d5e4d2a867 100644 --- a/iOSClient/Networking/NCConfigServer.swift +++ b/iOSClient/Networking/NCConfigServer.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2022 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCConfigServer.swift +// Nextcloud +// +// Created by Marino Faggiana on 05/12/22. +// Copyright © 2022 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import Foundation import UIKit diff --git a/iOSClient/Networking/NCNetworking+Download.swift b/iOSClient/Networking/NCNetworking+Download.swift index 4d9fe0c51c..22ecb91653 100644 --- a/iOSClient/Networking/NCNetworking+Download.swift +++ b/iOSClient/Networking/NCNetworking+Download.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCNetworking+Download.swift +// Nextcloud +// +// Created by Marino Faggiana on 07/02/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import UIKit import NextcloudKit @@ -327,3 +346,40 @@ extension NCNetworking { } } } + +class NCOperationDownload: ConcurrentOperation, @unchecked Sendable { + var metadata: tableMetadata + var selector: String + + init(metadata: tableMetadata, selector: String) { + self.metadata = tableMetadata.init(value: metadata) + self.selector = selector + } + + override func start() { + guard !isCancelled else { return self.finish() } + + metadata.session = NCNetworking.shared.sessionDownload + metadata.sessionError = "" + metadata.sessionSelector = selector + metadata.sessionTaskIdentifier = 0 + metadata.status = NCGlobal.shared.metadataStatusWaitDownload + +// let metadata = NCManageDatabase.shared.addMetadata(metadata) + +// NCNetworking.shared.download(metadata: metadata, withNotificationProgressTask: true) { +// } completion: { _, _ in +// self.finish() +// } + Task { + await download(withSelector: self.selector) + } + } + + private func download(withSelector selector: String = "") async { + await NCNetworking.shared.downloadFile(metadata: metadata) { _ in + self.finish() + } taskHandler: { _ in } + + } +} diff --git a/iOSClient/Networking/NCNetworking+LivePhoto.swift b/iOSClient/Networking/NCNetworking+LivePhoto.swift index e861b76b81..072b3b087f 100644 --- a/iOSClient/Networking/NCNetworking+LivePhoto.swift +++ b/iOSClient/Networking/NCNetworking+LivePhoto.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCNetworking+LivePhoto.swift +// Nextcloud +// +// Created by Marino Faggiana on 07/02/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import UIKit import NextcloudKit diff --git a/iOSClient/Networking/NCNetworking+Task.swift b/iOSClient/Networking/NCNetworking+Task.swift index f28830b583..1b6122252b 100644 --- a/iOSClient/Networking/NCNetworking+Task.swift +++ b/iOSClient/Networking/NCNetworking+Task.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCNetworking+Task.swift +// Nextcloud +// +// Created by Marino Faggiana on 24/08/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import Foundation import UIKit diff --git a/iOSClient/Networking/NCNetworking+Upload.swift b/iOSClient/Networking/NCNetworking+Upload.swift index 9467e9c0b7..6c985dbe7c 100644 --- a/iOSClient/Networking/NCNetworking+Upload.swift +++ b/iOSClient/Networking/NCNetworking+Upload.swift @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later - import UIKit import NextcloudKit import Alamofire @@ -45,6 +41,22 @@ extension NCNetworking { } taskHandler(task) } progressHandler: { progress in + Task { + guard await self.progressQuantizer.shouldEmit(serverUrlFileName: serverUrlFileName, fraction: progress.fractionCompleted) else { + return + } + + if let metadata { + await NCManageDatabase.shared.setMetadataProgress(ocId: metadata.ocId, progress: progress.fractionCompleted) + await self.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferProgressDidUpdate(progress: Float(progress.fractionCompleted), + totalBytes: progress.totalUnitCount, + totalBytesExpected: progress.completedUnitCount, + fileName: metadata.fileName, + serverUrl: metadata.serverUrl) + } + } + } progressHandler(progress.completedUnitCount, progress.totalUnitCount, progress.fractionCompleted) } @@ -378,6 +390,7 @@ extension NCNetworking { error: self.global.diagnosticProblemsUploadServerError) } } + await NCManageDatabase.shared.updateBadge() } // MARK: - diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 37629dadd1..4d78a1844e 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCNetworking+WebDAV.swift +// Nextcloud +// +// Created by Marino Faggiana on 07/02/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import UIKit import NextcloudKit @@ -465,6 +484,50 @@ extension NCNetworking { return .success } + + func renameMetadata(_ metadata: tableMetadata, + fileNameNew: String, + indexPath: IndexPath, + viewController: UIViewController?, + completion: @escaping (_ error: NKError) -> Void) { + + let permission = NCMetadataPermissions.permissionsContainsString(metadata.permissions, permissions: NCMetadataPermissions.permissionCanRename) + if (!metadata.permissions.isEmpty && permission == false) || + (metadata.status != global.metadataStatusNormal && metadata.status != global.metadataStatusWaitRename) { +// return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_no_permission_modify_file_")) +// let error = await NCNetworkingE2EERename().rename(metadata: metadata, fileNameNew: fileNameNew) +// DispatchQueue.main.async { completion(error) } + completion(NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_no_permission_modify_file_")) + } + + if metadata.isDirectoryE2EE { +#if !EXTENSION + if isOffline { +// return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) + completion(NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) + } + Task { + let error = await NCNetworkingE2EERename().rename(metadata: metadata, fileNameNew: fileNameNew) + if error != .success { +// NCContentPresenter().showError(error: error) + completion(error) + } + } +#endif + } else { + Task { + await self.transferDispatcher.notifyAllDelegatesAsync { delegate in + let status = self.global.metadataStatusWaitRename + await NCManageDatabase.shared.renameMetadata(fileNameNew: fileNameNew, ocId: metadata.ocId, status: status) + delegate.transferReloadData(serverUrl: metadata.serverUrl, status: status) + } + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterRenameFile, userInfo: ["serverUrl": metadata.serverUrl, "account": metadata.account, "error": NKError(errorCode: 0, errorDescription: ""), "ocId": metadata.ocId, "indexPath": indexPath]) + + } + + completion(NKError(errorCode: 0, errorDescription: "")) + } + } func renameFileOrFolder(metadata: tableMetadata) async -> NKError { let serverUrlFileNameSource = metadata.serverUrlFileName diff --git a/iOSClient/Networking/NCNetworking.swift b/iOSClient/Networking/NCNetworking.swift index c0684756d4..0c86cf84c8 100644 --- a/iOSClient/Networking/NCNetworking.swift +++ b/iOSClient/Networking/NCNetworking.swift @@ -37,6 +37,203 @@ protocol NCTransferDelegate: AnyObject { serverUrl: String) } +extension NCTransferDelegate { + func transferChange(status: String, + account: String, + fileName: String, + serverUrl: String, + selector: String?, + ocId: String, + destination: String?, + error: NKError) {} + func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) {} + func transferProgressDidUpdate(progress: Float, + totalBytes: Int64, + totalBytesExpected: Int64, + fileName: String, + serverUrl: String) {} +} + +/// Actor-based delegate dispatcher using weak references. +actor NCTransferDelegateDispatcher { + // Weak reference collection of delegates + private var transferDelegates = NSHashTable.weakObjects() + + /// Adds a delegate safely. + func addDelegate(_ delegate: NCTransferDelegate) { + transferDelegates.add(delegate) + } + + /// Remove a delegate safely. + func removeDelegate(_ delegate: NCTransferDelegate) { + transferDelegates.remove(delegate) + } + + /// Notifies all delegates. + func notifyAllDelegates(_ block: (NCTransferDelegate) -> Void) { + let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + for delegate in delegatesCopy { + block(delegate) + } + } + + func notifyAllDelegatesAsync(_ block: @escaping (NCTransferDelegate) async -> Void) async { + let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + for delegate in delegatesCopy { + await block(delegate) + } + } + + /// Notifies the delegate for a specific scene. + func notifyDelegate(forScene sceneIdentifier: String, _ block: (NCTransferDelegate) -> Void) { + let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + for delegate in delegatesCopy { + if delegate.sceneIdentifier == sceneIdentifier { + block(delegate) + } + } + } + + /// Notifies matching and non-matching delegates for a specific scene. + func notifyDelegates(forScene sceneIdentifier: String, + matching: (NCTransferDelegate) -> Void, + others: (NCTransferDelegate) -> Void) { + let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + for delegate in delegatesCopy { + if delegate.sceneIdentifier == sceneIdentifier { + matching(delegate) + } else { + others(delegate) + } + } + } +} + +/// A thread-safe registry for tracking in-flight `URLSessionTask` instances. +/// +/// Each task is associated with a string identifier (`identifier`) that you define, +/// allowing you to check whether a request is already running, avoid duplicates, +/// and cancel all active tasks at once. The registry automatically removes +/// completed tasks via `cleanupCompleted()` to keep memory usage compact. +/// +/// Typical use cases: +/// - Ensure only one task per identifier is active at a time. +/// - Query whether a specific request is still running (`isReading`). +/// - Forcefully stop a specific request (`cancel`). +/// - Forcefully stop all tasks when leaving a screen (`cancelAll`). +actor NetworkingTasks { + private var active: [(identifier: String, task: URLSessionTask)] = [] + + /// Returns whether there is an in-flight task for the given URL. + /// + /// A task is considered in-flight if its `state` is `.running` or `.suspended`. + /// - Parameter identifier: The identifier to check. + /// - Returns: `true` if a matching in-flight task exists; otherwise `false`. + func isReading(identifier: String) -> Bool { + // Drop finished/canceling tasks globally + cleanup() + + return active.contains { + $0.identifier == identifier && ($0.task.state == .running || $0.task.state == .suspended) + } + } + + /// Tracks a newly created `URLSessionTask` for the given identifier. + /// + /// If a running entry for the same identifier exists, it is removed before appending the new one. + /// - Parameters: + /// - identifier: The identifier associated with the task. + /// - task: The `URLSessionTask` to track. + func track(identifier: String, task: URLSessionTask) { + // Drop finished/canceling tasks globally + cleanup() + + active.removeAll { + $0.identifier == identifier && $0.task.state == .running + } + active.append((identifier, task)) + nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .start, message: "Start task for identifier: \(identifier)", consoleOnly: true) + } + + /// create a Identifier + /// + func createIdentifier(account: String? = nil, path: String? = nil, name: String) -> String { + if let account, + let path { + return account + "_" + path + "_" + name + } else if let path { + return path + "_" + name + } else { + return name + } + } + + /// Cancels and removes all tasks associated with the given id. + /// + /// - Parameter identifier: The identifier whose tasks should be canceled. + func cancel(identifier: String) { + // Drop finished/canceling tasks globally + cleanup() + + for element in active where element.identifier == identifier { + element.task.cancel() + nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .cancel, message: "Cancel task for identifier: \(identifier)", consoleOnly: true) + } + active.removeAll { + $0.identifier == identifier + } + } + + /// Cancels all tracked `URLSessionTask` and clears the registry. + /// + /// Call this when leaving the page/screen or when the operation must be forcefully stopped. + func cancelAll() { + active.forEach { + $0.task.cancel() + nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .cancel, message: "Cancel task with identifier: \($0.identifier)", consoleOnly: true) + } + active.removeAll() + } + + /// Removes tasks that have completed from the registry. + /// + /// Useful to keep the in-memory list compact during long-running operations. + func cleanup() { + active.removeAll { + $0.task.state == .completed || $0.task.state == .canceling + } + } +} + +/// Quantizes per-task progress updates to integer percentages (0...100). +/// Each (serverUrlFileName) pair is tracked separately, so you get +/// at most one update per integer percent for each transfer. +actor ProgressQuantizer { + private var lastPercent: [String: Int] = [:] + + /// Returns `true` only when integer percent changes (or hits 100). + /// + /// - Parameters: + /// - serverUrlFileName: The name of the file being transferred. + /// - fraction: Progress fraction [0.0 ... 1.0]. + func shouldEmit(serverUrlFileName: String, fraction: Double) -> Bool { + let percent = min(max(Int((fraction * 100).rounded(.down)), 0), 100) + + let last = lastPercent[serverUrlFileName] ?? -1 + guard percent != last || percent == 100 else { + return false + } + + lastPercent[serverUrlFileName] = percent + return true + } + + /// Clears stored state for a finished transfer. + func clear(serverUrlFileName: String) { + lastPercent.removeValue(forKey: serverUrlFileName) + } +} + class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { static let shared = NCNetworking() @@ -94,6 +291,7 @@ class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { let downloadAvatarQueue = Queuer(name: "downloadAvatarQueue", maxConcurrentOperationCount: 10, qualityOfService: .default) let fileExistsQueue = Queuer(name: "fileExistsQueue", maxConcurrentOperationCount: 10, qualityOfService: .default) #endif + let downloadQueue = Queuer(name: "downloadQueue", maxConcurrentOperationCount: NCBrandOptions.shared.httpMaximumConnectionsPerHostInDownload, qualityOfService: .default) // MARK: - init diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index 930a965320..9121911b0b 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -271,7 +271,6 @@ actor NCNetworkingProcess { await startTimer(interval: minInterval) } } else { - // Remove upload asset await removeUploadedAssetsIfNeeded() // Set Live Photo diff --git a/iOSClient/Networking/NCService.swift b/iOSClient/Networking/NCService.swift index f6715693d3..2987e2543c 100644 --- a/iOSClient/Networking/NCService.swift +++ b/iOSClient/Networking/NCService.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2018 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCService.swift +// Nextcloud +// +// Created by Marino Faggiana on 14/03/18. +// Copyright © 2018 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// import UIKit @preconcurrency import NextcloudKit diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 7d20929728..febf4069db 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -126,8 +126,8 @@ "_error_parameter_schema_" = "Wrong parameters, impossible to continue"; "_comments_" = "Comments"; "_sharing_" = "Sharing"; -"_details_" = "Share"; -//"_details_" = "Details"; +//"_details_" = "Share"; +"_details_" = "Details"; "_sub_details_" = "Subscription Details"; "_subscriptions_" = "Subscriptions"; "_dark_mode_" = "Dark mode"; @@ -232,8 +232,6 @@ "_settings_account_request_" = "Request account at startup"; "_print_" = "Print"; "_alias_" = "Alias"; -//"_alias_placeholder_" = "Write alias"; -//"_alias_footer_" = "Give your accounts a descriptive name such as Home, Office, School…"; "_alias_placeholder_" = "Write the alias"; "_alias_footer_" = "Give your account names a descriptive name such as Home, Office, School …"; "_chunk_size_mb_" = "Chunk size in MB"; @@ -283,7 +281,7 @@ //TOPasscodeSettingsWarningLabel.m "_1_failed_passcode_attempt" = "1 Failed Passcode Attempt"; -"_failed_passcode_attempts" = "%ld Failed Passcode Attempts"; +"_failed_passcode_attempts" = "%d Failed Passcode Attempts"; /* Background of the file listing view */ "_use_as_background_" = "Use it as a background"; @@ -324,7 +322,6 @@ "_status_message_" = "Status message"; "_status_message_placehorder_" = "What is your status?"; "_online_status_" = "Online status"; -"_clear_status_message_" = "Clear status message"; "_set_status_message_" = "Set status message"; "_clear_status_message_after_" = "Clear status after"; @@ -387,8 +384,8 @@ "_username_" = "Username"; "_change_credentials_" = "Change your credentials"; "_wifi_only_" = "Only use Wi-Fi connection"; -//"_settings_autoupload_" = "Auto upload photos"; -"_settings_autoupload_" = "Auto upload"; +"_settings_autoupload_" = "Auto upload photos"; +//"_settings_autoupload_" = "Auto upload"; "_app_version_" = "Application version"; "_app_in_use_" = "Application in use"; "_contact_by_email_" = "Contact us by email"; @@ -485,7 +482,6 @@ "_autoupload_change_location_footer_" = "Change folder used for \"Automatic upload of camera photos\" (if the option is enabled)"; "_autoupload_not_select_home_" = "Select a folder"; "_autoupload_save_album_" = "Copy photo or video into the photo album"; -"_autoupload_description_" = "Choose whether new photos or videos will be automatically uploaded to your account."; "_autoupload_fullphotos_" = "Upload the whole camera roll"; "_start_autoupload_" = "Turn on auto upload"; "_stop_autoupload_" = "Turn off auto upload"; @@ -629,11 +625,9 @@ "_add_offline_" = "Add to offline"; "_remove_passcode_" = "Remove password protection"; "_protect_passcode_" = "Protect with password"; -"_remove_favorites_" = "Unfavorite"; -"_add_favorites_" = "Favorite"; "_share_" = "Share"; "_reload_" = "Reload"; -"_open_in_" = "Open with..."; //"Open in …"; +"_open_in_" = "Open in …"; "_open_" = "Open …"; "_remove_local_file_" = "Remove locally"; "_add_local_" = "Add to local storage"; @@ -720,7 +714,7 @@ "_status_upload_error_" = "Error, waiting to upload"; "_select_media_folder_" = "Set Media folder"; "_media_viewimage_show_" = "Show only images"; -"_media_viewvideo_show_" = "Show only videos"; +"_media_viewvideo_show_" = "Show only video"; "_media_show_all_" = "Show both"; "_media_by_created_date_" = "Sort by created date"; "_media_by_upload_date_" = "Sort by upload date"; @@ -741,12 +735,10 @@ "_share_link_" = "Share link"; //"_share_link_" = "Share link"; "_share_link_" = "Link"; -//"_share_link_" = "Share link"; "_share_link_button_" = "Send link to …"; "_share_link_name_" = "Link name"; "_password_" = "Password"; "_share_password_" = "Password protected link"; -"_share_password_must_be_at_least_6_chars" = "Password must be at least 6 characters."; "_share_expirationdate_" = "Set expiration date for link"; "_date_" = "Date"; "_share_title_" = "Share"; @@ -804,42 +796,6 @@ "_new_comment_" = "New comment …"; "_edit_comment_" = "Edit comment"; "_delete_comment_" = "Delete comment"; -"_share_read_only_" = "View only"; -"_share_reshare_allowed_" = "Resharing is allowed."; -"_share_reshare_not_allowed_" = "Resharing is not allowed."; -"_sharing_message_" = "You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration."; -"_create_link_" = "Create Link"; -"personal_share_by_mail" = "Personal share by mail"; -"_your_shares_" = "Your Shares"; -"_share_linklabel_" = "Link '%@'"; -"_share_link_folder_" = "Link to folder"; -"_share_link_file_" = "Link to file"; -"no_shares_created" = "No shares created yet."; -"_advance_permissions_" = "Advanced permissions"; -"_send_new_email_" = "Send new email"; -"_apply_changes_" = "Apply changes"; -"_send_share_" = "Send share"; -"_PERMISSIONS_" = "PERMISSIONS"; -"share_editing_message" = "There are no editing functions for files unless you share them with MagentaCLOUD users."; -"_file_drop_message_" = "With File drop, only uploading is allowed. Only you can see files and folders that have been uploaded."; -"_custom_link_label" = "Your custom link label"; -"_set_password_" = "Set password"; -"_share_expiration_date_placeholder_"= "Expiration date for this share"; -"_share_download_limit_" = "Download Limit"; -"_share_download_limit_placeholder_" = "Enter download limit"; -"_share_download_limit_alert_empty_" = "Download limit cannot be empty."; -"_share_download_limit_alert_zero_" = "Download limit should be greater than 0."; -"_share_download_limit_alert_invalid_" = "Please enter a valid number for the download limit."; -"_share_remaining_download_" = "Downloads:"; -"_share_read_only_" = "View only"; -"_share_remaining_download_" = "Downloads:"; -"_share_editing_" = "Can edit"; -"_share_file_drop_" = "Filedrop only"; -"_share_hide_download_" = "Prevent download"; -//"_share_note_recipient_" = "YOUR MESSAGE"; -"_share_note_recipient_" = "Note to recipient"; -"_shareLinksearch_placeholder_" = "Contact name or email address"; -"_shareLinksearch_mail_placeholder_" = "Type a name or an email and press Enter"; "_share_reshare_allowed_" = "Resharing is allowed."; "_share_reshare_not_allowed_" = "Resharing is not allowed."; "_sharing_message_" = "You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration."; @@ -909,7 +865,6 @@ "_share_via_link_menu_password_label_" = "Password protect (%1$s)"; "_share_link_empty_exp_date_" = "You must select expiration date."; "_share_link_empty_note_message_" = "Please enter note."; -"_share_permission_should_not_be_empty_" = "Please select at least one permission."; "_remote_" = "Remote"; "_remote_group_" = "Remote group"; "_conversation_" = "Conversation"; @@ -1143,7 +1098,7 @@ "_voice_memo_filename_" = "Voice memo"; "_voice_memo_title_" = "Upload voice memo"; "_too_many_failed_passcode_attempts_error_" = "Too many failed passcode attempts. Please try again."; -//"Passcodes didn't match. Try again." = "Passcodes didn't match. Try again."; +"Passcodes didn't match. Try again." = "Passcodes didn't match. Try again."; "Delete" = "Delete"; "Cancel" = "Cancel"; "_certificate_details_" = "Certificate Server details"; @@ -1202,7 +1157,7 @@ "_waiting_for_" = "Waiting for:"; "_waiting_" = "Waiting …"; "_reachable_wifi_" = "network reachable via Wi-Fi or cable"; -"_ITMS-90076_" = "Due to a change in the MagentaCLOUD application identifier, the settings and password for accessing your cloud are reset, so please re-enter your account data and check your Settings. We are sorry about that."; +"_ITMS-90076_" = "Due to a change in the Nextcloud application identifier, the settings and password for accessing your cloud are reset, so please re-enter your account data and check your Settings. We are sorry about that."; "_password_not_present_" = "Please re-insert your credentials."; "_copy_passphrase_" = "Copy passphrase"; "_ok_copy_passphrase_" = "OK and copy passphrase"; @@ -1360,7 +1315,7 @@ "_delete_in_progress_" = "Delete in progress …"; "_download_in_progress_" = "Download in progress …"; "_upload_in_progress_" = "Upload in progress …"; -"_transfer_in_progress_" = "Transfer in progress …"; +"_transfer_in_progress_" = "Transfer in progress …"; "_in_waiting_" = "In waiting"; "_in_progress_" = "In progress"; "_in_error_" = "In error"; @@ -1575,46 +1530,3 @@ You can stop it at any time, adjust the settings, and enable it again."; "_e2ee_setup_store_privatekey_" = "Private key generation returned no usable key"; "_e2ee_setup_verify_publickey_" = "Public key does not match the certificate"; "_e2ee_setup_generate_passphrase_" = "Unable to generate a passphrase"; - -// MARK: Albums -"_albums_loading_popup_desc_" = "Wait a moment..."; -"_albums_list_nav_title_" = "Albums"; -"_albums_list_new_album_btn_" = "New"; -"_albums_list_loading_msg_" = "Loading..."; -"_albums_list_error_msg_" = "Unable to load albums. Please try again later!"; -"_albums_list_album_name_validation_nonempty_" = "Album name cannot be empty."; -"_albums_list_album_name_validation_min_length_" = "Album name must be at least 3 characters."; -"_albums_list_album_name_validation_max_length_" = "Album name cannot be more than 30 characters."; -"_albums_list_album_name_validation_specials_" = "Album name cannot contain slashes."; -"_albums_list_empty_heading_" = "Create\nAlbums\nfor your\nPhotos"; -"_albums_list_empty_subheading_" = "You can organize all your photos in as many albums as you like. You haven't created an album yet."; -"_albums_list_empty_new_album_btn_" = "Create album"; -"_albums_list_own_albums_heading_" = "My albums"; -"_albums_list_entities_" = "Items"; -"_albums_list_new_album_popup_title_" = "Create new Album"; -"_albums_list_new_album_popup_desc_" = "Please enter an album name between 3 and 30 characters."; -"_albums_list_new_album_popup_hint_" = "Album's name"; -"_albums_list_new_album_popup_positive_btn_" = "Create"; -"_albums_list_new_album_popup_negative_btn_" = "Cancel"; -"_albums_list_rename_album_popup_title_" = "Rename Album"; -"_albums_list_rename_album_popup_desc_" = "Please enter new album name between 3 and 30 characters."; -"_albums_list_rename_album_popup_hint_" = "Album's new name"; -"_albums_list_rename_album_popup_positive_btn_" = "Rename"; -"_albums_list_rename_album_popup_negative_btn_" = "Cancel"; -"_albums_photo_selection_sheet_title_" = "Select items"; -"_albums_photo_selection_sheet_back_btn_" = "Back"; -"_albums_photo_selection_sheet_done_btn_" = "Done"; -"_albums_photos_loading_msg_" = "Loading photos..."; -"_albums_photos_error_msg_" = "Unable to load photos. Please try again later!"; -"_albums_photos_empty_heading_" = "All that's\nmissing are\nyour photos"; -"_albums_photos_empty_subheading_" = "You can add as many photos as you like. A photo can also belong to more than one album."; -"_albums_photos_empty_add_photos_btn_" = "Add photos"; -"_albums_photos_add_photos_btn_" = "Add"; -"_albums_photos_rename_album_btn_" = "Rename Album"; -"_albums_photos_delete_album_btn_" = "Delete Album"; -"_albums_delete_album_popup_title_" = "Delete Album?"; -"_albums_delete_album_popup_desc_" = "Are you sure you want to delete this album? This action cannot be undone."; -"_albums_delete_album_popup_positive_btn_" = "Delete"; -"_albums_delete_album_popup_negative_btn_" = "Cancel"; -"_add_to_album" = "Add to Album"; -"_remove_from_album_" = "Remove from Album"; From 95ed839dc2b5bae15b3eae52fc5fe4e5be2a0680 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Thu, 9 Apr 2026 18:59:22 +0530 Subject: [PATCH 40/40] NMC 1984 - Privacy policy customisation updated changes with 10.3.0 version --- .../Data/NCManageDatabase+LocalFile.swift | 40 +- .../Data/NCManageDatabase+Metadata.swift | 66 --- .../NCCollectionViewCommon+SyncMetadata.swift | 14 +- .../NCCollectionViewCommon.swift | 386 ++++++++++-------- .../NCBackgroundLocationUploadManager.swift | 19 +- iOSClient/NCGlobal.swift | 24 +- iOSClient/Networking/NCConfigServer.swift | 25 +- .../Networking/NCNetworking+Download.swift | 62 +-- .../Networking/NCNetworking+LivePhoto.swift | 25 +- iOSClient/Networking/NCNetworking+Task.swift | 25 +- .../Networking/NCNetworking+Upload.swift | 21 +- .../Networking/NCNetworking+WebDAV.swift | 69 +--- iOSClient/Networking/NCNetworking.swift | 198 --------- .../Networking/NCNetworkingProcess.swift | 1 + iOSClient/Networking/NCService.swift | 25 +- .../PrivacySettingsViewController.swift | 9 +- .../en.lproj/Localizable.strings | 108 ++++- 17 files changed, 391 insertions(+), 726 deletions(-) diff --git a/iOSClient/Data/NCManageDatabase+LocalFile.swift b/iOSClient/Data/NCManageDatabase+LocalFile.swift index 334e5fba25..2146dec1ad 100644 --- a/iOSClient/Data/NCManageDatabase+LocalFile.swift +++ b/iOSClient/Data/NCManageDatabase+LocalFile.swift @@ -29,17 +29,14 @@ extension NCManageDatabase { // MARK: - Realm Write + /// Adds or updates multiple local file entries corresponding to the given metadata array. + /// Uses async Realm read + single write transaction. Assumes `tableLocalFile` has a primary key. /// - Parameters: - /// - metadata: The `tableMetadata` containing file details. - /// - offline: Optional flag to mark the file as available offline. - /// - Returns: Nothing. Realm write is performed asynchronously. - func addLocalFileAsync(metadata: tableMetadata, offline: Bool? = nil) async { - // Read (non-blocking): safely detach from Realm thread - let existing: tableLocalFile? = performRealmRead { realm in - realm.objects(tableLocalFile.self) - .filter(NSPredicate(format: "ocId == %@", metadata.ocId)) - .first - .map { tableLocalFile(value: $0) } + /// - metadatas: Array of `tableMetadata` to map into `tableLocalFile`. + /// - offline: Optional override for the `offline` flag applied to all items. + func addLocalFilesAsync(metadatas: [tableMetadata], offline: Bool? = nil) async { + guard !metadatas.isEmpty else { + return } // Extract ocIds for efficient lookup @@ -62,19 +59,20 @@ extension NCManageDatabase { // Reuse existing object or create a new one let local = existingMap[metadata.ocId] ?? tableLocalFile() - addObject.account = metadata.account - addObject.etag = metadata.etag - addObject.exifDate = NSDate() - addObject.exifLatitude = "-1" - addObject.exifLongitude = "-1" - addObject.ocId = metadata.ocId - addObject.fileName = metadata.fileName + local.account = metadata.account + local.etag = metadata.etag + local.exifDate = NSDate() + local.exifLatitude = "-1" + local.exifLongitude = "-1" + local.ocId = metadata.ocId + local.fileName = metadata.fileName - if let offline { - addObject.offline = offline - } + if let offline { + local.offline = offline + } - realm.add(addObject, update: .all) + realm.add(local, update: .all) + } } } diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index 5c3b840691..67ed7ad2f2 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -151,37 +151,10 @@ extension tableMetadata { (fileNameView as NSString).deletingPathExtension } - var isRenameable: Bool { - if !NCMetadataPermissions.canRename(self) { - return false - } - if lock { - return false - } - if !isDirectoryE2EE && e2eEncrypted { - return false - } - return true - } - - var isPrintable: Bool { - if isDocumentViewableOnly { - return false - } - if ["application/pdf", "com.adobe.pdf"].contains(contentType) || contentType.hasPrefix("text/") || classFile == NKTypeClassFile.image.rawValue { - return true - } - return false - } - var isSavebleInCameraRoll: Bool { return (classFile == NKTypeClassFile.image.rawValue && contentType != "image/svg+xml") || classFile == NKTypeClassFile.video.rawValue } - var isDocumentViewableOnly: Bool { - sharePermissionsCollaborationServices == NCPermissions().permissionReadShare && classFile == NKTypeClassFile.document.rawValue - } - var isAudioOrVideo: Bool { return classFile == NKTypeClassFile.audio.rawValue || classFile == NKTypeClassFile.video.rawValue } @@ -371,17 +344,6 @@ extension tableMetadata { return !lock || (lockOwner == user && lockOwnerType == 0) } - // Return if is sharable - func isSharable() -> Bool { - guard let capabilities = NCNetworking.shared.capabilities[account] else { - return false - } - if !capabilities.fileSharingApiEnabled || (capabilities.e2EEEnabled && isDirectoryE2EE), !e2eEncrypted { - return false - } - return !e2eEncrypted - } - /// Returns a detached (unmanaged) deep copy of the current `tableMetadata` object. /// /// - Note: Primitive properties and lists of primitive values (for example `shareType`) @@ -969,34 +931,6 @@ extension NCManageDatabase { .map { $0.detachedCopy() } } ?? [] } - - func getMediaMetadatas(predicate: NSPredicate, sorted: String? = nil, ascending: Bool = false) -> ThreadSafeArray? { - - do { - let realm = try Realm() - if let sorted { - var results: [tableMetadata] = [] - switch sorted {//NCPreferences().mediaSortDate { - case "date": - results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.date as Date) > ($1.date as Date) } - case "creationDate": - results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.creationDate as Date) > ($1.creationDate as Date) } - case "uploadDate": - results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.uploadDate as Date) > ($1.uploadDate as Date) } - default: - let results = realm.objects(tableMetadata.self).filter(predicate) - return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) - } - return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) - } else { - let results = realm.objects(tableMetadata.self).filter(predicate) - return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) - } - } catch let error as NSError { -// NextcloudKit.shared.nkCommonInstance.writeLog("Could not access database: \(error)") - } - return nil - } func getMetadatas(predicate: NSPredicate, sortedByKeyPath: String, diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift index 571306d2de..5e0254858d 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SyncMetadata.swift @@ -21,7 +21,7 @@ extension NCCollectionViewCommon { // If a sync task is already running, do not start a new one if let task = syncMetadatasTask, !task.isCancelled { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .info, message: "Exit: Another sync is already running. Skipping this one.", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .info, message: "Exit: Another sync is already running. Skipping this one.", consoleOnly: true) return } @@ -45,9 +45,9 @@ extension NCCollectionViewCommon { func stopSyncMetadata() async { if let task = syncMetadatasTask { if task.isCancelled { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .stop, message: "Sync Metadata for \(self.serverUrl) was already cancelled.", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .stop, message: "Sync Metadata for \(self.serverUrl) was already cancelled.", consoleOnly: true) } else { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .stop, message: "Stopping active Sync Metadata for \(self.serverUrl).", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .stop, message: "Stopping active Sync Metadata for \(self.serverUrl).", consoleOnly: true) } } @@ -74,7 +74,7 @@ extension NCCollectionViewCommon { return } let identifier = self.serverUrl + "_syncMetadata" - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .start, message: "Start Sync Metadata for \(self.serverUrl)") + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .start, message: "Start Sync Metadata for \(self.serverUrl)") // Always cancel and clear all tracked URLSessionTask on any exit path defer { @@ -85,7 +85,7 @@ extension NCCollectionViewCommon { // If a readFile for this serverUrl is already in-flight, do nothing if await networking.networkingTasks.isReading(identifier: identifier) { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .debug, message: "ReadFile for this \(self.serverUrl) is already in-flight.", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .debug, message: "ReadFile for this \(self.serverUrl) is already in-flight.", consoleOnly: true) return } @@ -151,9 +151,9 @@ extension NCCollectionViewCommon { // If this folder failed, skip it but keep processing others if resultsReadFolder.error == .success { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .network, message: "Read correctly: \(serverUrl)", consoleOnly: true) + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .network, message: "Read correctly: \(serverUrl)", consoleOnly: true) } else { - nkLog(tag: global.logSpeedUpSyncMetadata, emoji: .error, message: "Read failed for \(serverUrl) with error: \(resultsReadFolder.error.errorDescription)") + nkLog(tag: global.logTagSpeedUpSyncMetadata, emoji: .error, message: "Read failed for \(serverUrl) with error: \(resultsReadFolder.error.errorDescription)") return } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 9452410c87..f694bc9220 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -4,14 +4,12 @@ import UIKit import SwiftUI -import Realm import RealmSwift import NextcloudKit import EasyTipView import LucidBanner -import MoEngageInApps -class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionFirstHeaderDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { +class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { @IBOutlet weak var collectionView: UICollectionView! @@ -54,12 +52,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var tabBarSelect: NCCollectionViewCommonSelectTabBar? var attributesZoomIn: UIMenuElement.Attributes = [] var attributesZoomOut: UIMenuElement.Attributes = [] - var syncMetadatasTask: Task? - let maxImageGrid: CGFloat = 7 -// var headerMenu: NCSectionFirstHeader? - var tipViewAccounts: EasyTipView? - var selectableDataSource: [RealmSwiftObject] { dataSource.getMetadataSourceForAllSections() } + var syncMetadatasTask: Task? // DECLARE var layoutKey = "" @@ -167,7 +161,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return self.serverUrl == self.utilityFileSystem.getHomeServer(session: self.session) && capabilities.recommendations } - internal let debouncer = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerReloadDataSource = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerReloadData = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerGetServerData = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerNetworkSearch = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) // MARK: - View Life Cycle @@ -250,29 +247,28 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let appVersion = Bundle.main.infoDictionary?["CFBundleInfoDictionaryVersion"] as? String UserDefaults.standard.set(appVersion, forKey: "CurrentAppVersion") } - + registerForTraitChanges([UITraitUserInterfaceStyle.self]) { [weak self] (view: NCCollectionViewCommon, _) in guard let self else { return } self.sectionFirstHeader?.setRichWorkspaceColor(style: view.traitCollection.userInterfaceStyle) } - NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterChangeTheming), object: nil, queue: .main) { [weak self] _ in - guard let self else { return } - self.collectionView.reloadData() + NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterChangeTheming), object: nil, queue: .main) { _ in + let serverUrl = self.serverUrl + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: serverUrl) + } + } } - - registerForTraitChanges([UITraitUserInterfaceStyle.self]) { [weak self] (view: NCCollectionViewCommon, _) in - guard let self else { return } - self.sectionFirstHeader?.setRichWorkspaceColor(style: view.traitCollection.userInterfaceStyle) + NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterUserInteractionMonitor), object: nil, queue: .main) { _ in + Task { + await self.debouncerReloadData.resume() + } } - NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterChangeTheming), object: nil, queue: .main) { [weak self] _ in - guard let self else { return } - self.collectionView.reloadData() - } - NotificationCenter.default.addObserver(self, selector: #selector(updateIcons), name: NSNotification.Name(rawValue: global.notificationCenterUpdateIcons), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateIcons), name: NSNotification.Name(rawValue: global.notificationCenterUpdateIcons), object: nil) @@ -285,6 +281,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) +// AnalyticsHelper.shared.displayInAppNotification() + if titlePreviusFolder != nil { navigationController?.navigationBar.topItem?.title = titlePreviusFolder } @@ -297,6 +295,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS isEditMode = false Task { + await NCNetworking.shared.transferDispatcher.addDelegate(self) + await (self.navigationController as? NCMainNavigationController)?.setNavigationLeftItems() await (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() } @@ -383,7 +383,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } @objc func updateIcons() { - collectionView.reloadData() +// collectionView.reloadData() + Task { + await self.reloadDataSource() + } } func isApplicationUpdated() -> Bool { @@ -852,12 +855,9 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return NCBrandOptions.shared.brand } - func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } - @MainActor func startGUIGetServerData() { self.dataSource.setGetServerData(false) - self.collectionView.reloadData() // Don't show spinner on iPad root folder if UIDevice.current.userInterfaceIdiom == .pad, @@ -887,61 +887,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.navigationItem.titleView = nil self.navigationItem.title = self.titleCurrentFolder } - - func resetPlusButtonAlpha(animated: Bool = true) { } - - func isHiddenPlusButton(_ isHidden: Bool) { } - - // MARK: - Empty - - func emptyDataSetView(_ view: NCEmptyView) { - - self.emptyDataSet?.setOffset(getHeaderHeight()) - if isSearchingMode { - view.emptyImage.image = UIImage(named: "search")?.image(color: .gray, size: UIScreen.main.bounds.width) - if self.dataSourceTask?.state == .running { - view.emptyTitle.text = NSLocalizedString("_search_in_progress_", comment: "") - } else { - view.emptyTitle.text = NSLocalizedString("_search_no_record_found_", comment: "") - } - view.emptyDescription.text = NSLocalizedString("_search_instruction_", comment: "") - } else if self.dataSourceTask?.state == .running { - view.emptyImage.image = UIImage(named: "networkInProgress")?.image(color: .gray, size: UIScreen.main.bounds.width) - view.emptyTitle.text = NSLocalizedString("_request_in_progress_", comment: "") - view.emptyDescription.text = "" - } else { - if serverUrl.isEmpty { - view.emptyImage.image = emptyImage - view.emptyTitle.text = NSLocalizedString(emptyTitle, comment: "") - view.emptyDescription.text = NSLocalizedString(emptyDescription, comment: "") - } else { - view.emptyImage.image = UIImage(named: "folder_nmcloud") - view.emptyTitle.text = NSLocalizedString("_files_no_files_", comment: "") - view.emptyDescription.text = NSLocalizedString("_no_file_pull_down_", comment: "") - } - } - - let spinner = UIActivityIndicatorView(style: .medium) - spinner.startAnimating() - - let container = UIView() - container.translatesAutoresizingMaskIntoConstraints = false - container.addSubview(spinner) - - spinner.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - spinner.centerXAnchor.constraint(equalTo: container.centerXAnchor), - spinner.centerYAnchor.constraint(equalTo: container.centerYAnchor) - ]) - - self.navigationItem.titleView = container - } - - @MainActor - func restoreDefaultTitle() { - self.navigationItem.titleView = nil - self.navigationItem.title = self.titleCurrentFolder - } // MARK: - SEARCH @@ -952,7 +897,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS searchController?.searchBar.alpha = 1 } else { searchController?.searchBar.alpha = 0.3 - } } @@ -983,6 +927,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.networking.cancelUnifiedSearchFiles() self.isSearchingMode = false + self.networkSearchInProgress = false self.literalSearch = "" self.providers?.removeAll() self.dataSource.removeAll() @@ -1005,11 +950,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) { guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } - - NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) -// NCDownloadAction.shared.openShare(viewController: self, metadata: metadata, page: .sharing) TealiumHelper.shared.trackEvent(title: "magentacloud-app.filebrowser.sharing", data: ["": ""]) appDelegate.adjust.trackEvent(TriggerEvent(Sharing.rawValue)) + + NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) } func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { @@ -1017,30 +961,30 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS toggleMenu(metadata: metadata, image: image, sender: sender) } - func tapRichWorkspace(_ sender: Any) { - if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { - if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { - viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" - viewerRichWorkspace.serverUrl = serverUrl - viewerRichWorkspace.delegate = self - - navigationController.modalPresentationStyle = .fullScreen - self.present(navigationController, animated: true, completion: nil) - } - } - } +// func tapRichWorkspace(_ sender: Any) { +// if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { +// if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { +// viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" +// viewerRichWorkspace.serverUrl = serverUrl +// viewerRichWorkspace.delegate = self +// +// navigationController.modalPresentationStyle = .fullScreen +// self.present(navigationController, animated: true, completion: nil) +// } +// } +// } - func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { - toggleMenu(metadata: metadata, image: image, sender: sender) - } +// func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { +// toggleMenu(metadata: metadata, image: image, sender: sender) +// } - func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { - unifiedSearchMore(metadataForSection: metadataForSection) - } +// func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { +// unifiedSearchMore(metadataForSection: metadataForSection) +// } - func tapRecommendations(with metadata: tableMetadata) { - didSelectMetadata(metadata, withOcIds: false) - } +// func tapRecommendations(with metadata: tableMetadata) { +// didSelectMetadata(metadata, withOcIds: false) +// } func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } @@ -1147,7 +1091,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS serverUrlFileName: serverUrlFileName) { _ in } progressHandler: { _, _, fractionCompleted in Task {@MainActor in - LucidBanner.shared.update(progress: fractionCompleted, for: token) + LucidBanner.shared.update( + payload: LucidBannerPayload.Update(progress: fractionCompleted), + for: token + ) } } @@ -1167,14 +1114,12 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS fileName: fileName) Task { await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: serverUrl, requestData: true, status: nil) + delegate.transferReloadDataSource(serverUrl: self.serverUrl, requestData: true, status: nil) } } } else { - Task {@MainActor in - await showErrorBanner(scene: scene, - errorDescription: resultsUpload.error.errorDescription, - errorCode: resultsUpload.error.errorCode) + Task { + await showErrorBanner(scene: scene, text: resultsUpload.error.errorDescription) } } } @@ -1185,52 +1130,21 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // MARK: - DataSource - @objc func reloadDataSource() { - - // get auto upload folder - autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() - autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: session.urlBase, userId: session.userId, account: session.account) - // get layout for view - layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) - // set GroupField for Grid - if !isSearchingMode && layoutForView?.layout == NCGlobal.shared.layoutGrid { - groupByField = "classFile" - } else { - groupByField = "name" - } - if isSearchingMode { - isDirectoryEncrypted = false - } else { - isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) - } - - DispatchQueue.main.async { -// UIView.transition(with: self.collectionView, -// duration: 0.20, -// options: .transitionCrossDissolve, -// animations: { self.collectionView.reloadData() }, -// completion: nil) -// -// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() - if isRecommendationActived { - Task.detached { + @MainActor + func reloadDataSource() async { + if !isSearchingMode { + Task.detached { + if await self.isRecommendationActived() { await self.networking.createRecommendations(session: self.session, serverUrl: self.serverUrl, collectionView: self.collectionView) } } } - DispatchQueue.main.async { -// UIView.transition(with: self.collectionView, -// duration: 0.20, -// options: .transitionCrossDissolve, -// animations: { self.collectionView.reloadData() }, -// completion: nil) -// -// (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() - self.refreshControl.endRefreshing() - self.collectionView.reloadData() - self.setNavigationRightItems() + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: self.serverUrl) } + + await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() } func getServerData(forced: Bool = false) async { } @@ -1284,8 +1198,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } completion: { metadatasSearch, error in Task { guard let metadatasSearch, - error == .success, - self.isSearchingMode + error == .success, + self.isSearchingMode else { self.networkSearchInProgress = false await self.reloadDataSource() @@ -1312,7 +1226,11 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS guard let metadataForSection = metadataForSection, let lastSearchResult = metadataForSection.lastSearchResult, let cursor = lastSearchResult.cursor, let term = literalSearch else { return } metadataForSection.unifiedSearchInProgress = true - self.collectionView?.reloadData() + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: nil) + } + } self.networking.unifiedSearchFilesProvider(id: lastSearchResult.id, term: term, limit: 5, cursor: cursor, account: session.account) { task in self.searchDataSourceTask = task @@ -1321,12 +1239,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } completion: { _, searchResult, metadatas, error in if error != .success { - Task {@MainActor in - await showErrorBanner( - controller: self.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode - ) + Task { + await showErrorBanner(controller: self.controller, text: error.errorDescription) } } @@ -1334,8 +1248,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS guard let searchResult = searchResult, let metadatas = metadatas else { return } self.dataSource.appendMetadatasToSection(metadatas, metadataForSection: metadataForSection, lastSearchResult: searchResult) - DispatchQueue.main.async { - self.collectionView?.reloadData() + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: nil) + } } } } @@ -1368,21 +1284,38 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } - func pushViewController(viewController: UIViewController) { - if pushed { return } + // MARK: - Header size - pushed = true - navigationController?.pushViewController(viewController, animated: true) - } + func getHeaderHeight(section: Int) -> (heightHeaderRichWorkspace: CGFloat, + heightHeaderRecommendations: CGFloat, + heightHeaderSection: CGFloat) { + var heightHeaderRichWorkspace: CGFloat = 0 + var heightHeaderRecommendations: CGFloat = 0 + var heightHeaderSection: CGFloat = 0 - // MARK: - Header size - - func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { - if headerMenuTransferView, - NCNetworking.shared.isOnline, - let results = database.getResultsMetadatas(predicate: NSPredicate(format: "status IN %@", [global.metadataStatusWaitUpload, global.metadataStatusUploading])), - !results.isEmpty { - return Array(results) + if showDescription, + !isSearchingMode, + let richWorkspaceText = self.richWorkspaceText, + !richWorkspaceText.trimmingCharacters(in: .whitespaces).isEmpty { + heightHeaderRichWorkspace = UIScreen.main.bounds.size.height / 6 + } + + if isRecommendationActived, + !isSearchingMode, + NCPreferences().showRecommendedFiles, + !self.database.getRecommendedFiles(account: self.session.account).isEmpty { + heightHeaderRecommendations = self.heightHeaderRecommendations + heightHeaderSection = self.heightHeaderSection + } + + if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { + if section == 0 { + return (heightHeaderRichWorkspace, heightHeaderRecommendations, self.heightHeaderSection) + } else { + return (0, 0, self.heightHeaderSection) + } + } else { + return (heightHeaderRichWorkspace, heightHeaderRecommendations, heightHeaderSection) } } @@ -1654,4 +1587,105 @@ extension NCCollectionViewCommon: NCTransferDelegate { } } } + + func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } +} + +extension NCCollectionViewCommon: NCSectionFirstHeaderDelegate { + func tapRichWorkspace(_ sender: Any) { + if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { + if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { + viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" + viewerRichWorkspace.serverUrl = serverUrl + viewerRichWorkspace.delegate = self + + navigationController.modalPresentationStyle = .fullScreen + self.present(navigationController, animated: true, completion: nil) + } + } + } + + func tapRecommendations(with metadata: tableMetadata) { + didSelectMetadata(metadata, withOcIds: false) + } +} + +extension NCCollectionViewCommon: NCSectionFooterDelegate { + func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { + unifiedSearchMore(metadataForSection: metadataForSection) + } +} + +extension NCCollectionViewCommon: NCTransferDelegate { + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + + func transferReloadData(serverUrl: String?) { + Task { + await self.debouncerReloadData.call({ + self.collectionView.reloadData() + }, immediate: true) + } + } + + func transferChange(status: String, + account: String, + fileName: String, + serverUrl: String, + selector: String?, + ocId: String, + destination: String?, + error: NKError) { + Task { + if error != .success, + error.errorCode != global.errorResourceNotFound { + await showErrorBanner(controller: self.controller, text: error.errorDescription) + } + guard session.account == account else { + return + } + + if status == self.global.networkingStatusCreateFolder { + if serverUrl == self.serverUrl, + selector != self.global.selectorUploadAutoUpload, + let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { + self.pushMetadata(metadata) + } + return + } + + if self.isSearchingMode { + await self.debouncerNetworkSearch.call { + self.networkSearch() + } + } else if self.serverUrl == serverUrl || destination == self.serverUrl || self.serverUrl.isEmpty { + await self.debouncerReloadDataSource.call { + await self.reloadDataSource() + } + } + } + } + + func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { + Task { + if self.isSearchingMode { + await self.debouncerNetworkSearch.call { + self.networkSearch() + } + return + } + + if requestData && (self.serverUrl == serverUrl || serverUrl == nil) { + await self.debouncerGetServerData.call { + await self.getServerData() + } + return + } + + if self.serverUrl == serverUrl || serverUrl == nil { + await self.debouncerReloadDataSource.call { + await self.reloadDataSource() + } + } + } + } } diff --git a/iOSClient/NCBackgroundLocationUploadManager.swift b/iOSClient/NCBackgroundLocationUploadManager.swift index 8f63b82e9d..154fda662d 100644 --- a/iOSClient/NCBackgroundLocationUploadManager.swift +++ b/iOSClient/NCBackgroundLocationUploadManager.swift @@ -1,10 +1,6 @@ -// -// NCBackgroundLocationUploadManager.swift -// Nextcloud -// -// Created by Marino Faggiana on 06/06/25. -// Copyright © 2025 Marino Faggiana. All rights reserved. -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import CoreLocation import NextcloudKit @@ -106,15 +102,16 @@ class NCBackgroundLocationUploadManager: NSObject, CLLocationManagerDelegate { } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + // Must work only in background guard isAppInBackground else { return } // Open Realm - if database.openRealmBackground() { - let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! - let location = locations.last - nkLog(tag: self.global.logTagLocation, emoji: .start, message: "Triggered by location change: \(location?.coordinate.latitude ?? 0), \(location?.coordinate.longitude ?? 0)") + guard NCManageDatabase.shared.openRealmBackground() else { + nkLog(tag: self.global.logTagLocation, emoji: .error, message: "Failed to open Realm in Location Manager") + return + } let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! let location = locations.last diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index c7c11d0a81..2fcf152ceb 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -50,7 +50,7 @@ final class NCGlobal: Sendable { // let avatarSize: Int = 128 * Int(UIScreen.main.scale) let avatarSizeRounded: Int = 128 - + // Preview size // let size1024: CGSize = CGSize(width: 1024, height: 1024) @@ -104,6 +104,7 @@ final class NCGlobal: Sendable { let layoutViewOffline = "LayoutOffline" let layoutViewFavorite = "LayoutFavorite" let layoutViewFiles = "LayoutFiles" + let layoutViewTransfers = "LayoutTransfers" let layoutViewRecent = "LayoutRecent" let layoutViewShares = "LayoutShares" let layoutViewShareExtension = "LayoutShareExtension" @@ -125,7 +126,6 @@ final class NCGlobal: Sendable { let heightFooterButton: CGFloat = 30 let endHeightFooter: CGFloat = 85 - // Text - OnlyOffice - Collabora - QuickLook // let editorText = "text" @@ -229,7 +229,7 @@ final class NCGlobal: Sendable { let selectorSynchronizationOffline = "synchronizationOffline" let selectorPrint = "print" let selectorDeleteFile = "deleteFile" - + // Metadata : Status // // 0 normal @@ -259,6 +259,7 @@ final class NCGlobal: Sendable { let metadataStatusForScreenAwake = [-1, -2, 1, 2] let metadataStatusHideInView = [1, 2, 3, 11] let metadataStatusWaitWebDav = [10, 11, 12, 13, 14, 15] + let metadataStatusTransfers = [-2, -3, 2, 3, 10, 11, 12, 13, 14, 15] let metadatasStatusInWaiting = [-1, 1, 10, 11, 12, 13, 14, 15] let metadatasStatusInProgress = [-2, 2] @@ -275,16 +276,22 @@ final class NCGlobal: Sendable { let notificationCenterChangeTheming = "changeTheming" // userInfo: account let notificationCenterRichdocumentGrabFocus = "richdocumentGrabFocus" let notificationCenterReloadDataNCShare = "reloadDataNCShare" - let notificationCenterDidCreateShareLink = "didCreateShareLink" - let notificationCenterCloseRichWorkspaceWebView = "closeRichWorkspaceWebView" let notificationCenterReloadAvatar = "reloadAvatar" let notificationCenterClearCache = "clearCache" let notificationCenterCheckUserDelaultErrorDone = "checkUserDelaultErrorDone" // userInfo: account, controller let notificationCenterServerDidUpdate = "serverDidUpdate" // userInfo: account let notificationCenterNetworkReachability = "networkReachability" + let notificationCenterDidCreateShareLink = "didCreateShareLink" + let notificationCenterDeleteFile = "deleteFile" // userInfo: [ocId], error + let notificationCenterCopyMoveFile = "copyMoveFile" // userInfo: [ocId] serverUrl, account, dragdrop, type (copy, move) + let notificationCenterMoveFile = "moveFile" // userInfo: [ocId], [indexPath], error + let notificationCenterCopyFile = "copyFile" // userInfo: [ocId], [indexPath], error let notificationCenterRenameFile = "renameFile" // userInfo: serverUrl, account, error + let notificationCenterFavoriteFile = "favoriteFile" // userInfo: ocId, serverUrl + let notificationCenterFileExists = "fileExists" // userInfo: ocId, fileExists + let notificationCenterReloadDataSource = "reloadDataSource" // userInfo: serverUrl?, clearDataSource let notificationCenterMenuSearchTextPDF = "menuSearchTextPDF" let notificationCenterMenuGotToPageInPDF = "menuGotToPageInPDF" @@ -292,6 +299,7 @@ final class NCGlobal: Sendable { let notificationCenterOpenMediaDetail = "openMediaDetail" // userInfo: ocId let notificationCenterDismissScanDocument = "dismissScanDocument" + let notificationCenterDismissUploadAssets = "dismissUploadAssets" let notificationCenterEnableSwipeGesture = "enableSwipeGesture" let notificationCenterDisableSwipeGesture = "disableSwipeGesture" @@ -416,7 +424,7 @@ final class NCGlobal: Sendable { let keyFileNameAutoUploadType = "fileNameAutoUploadType" let keyFileNameOriginal = "fileNameOriginal" let keyFileNameOriginalAutoUpload = "fileNameOriginalAutoUpload" - + // LOG TAG // let logTagTask = "BGT" @@ -435,6 +443,10 @@ final class NCGlobal: Sendable { // let udMigrationMultiDomains = "migrationMultiDomains" let udLastVersion = "lastVersion" + + // Album + // + let selectedTabIndexAlbum: Int = 3 } /** diff --git a/iOSClient/Networking/NCConfigServer.swift b/iOSClient/Networking/NCConfigServer.swift index d5e4d2a867..f82b466647 100644 --- a/iOSClient/Networking/NCConfigServer.swift +++ b/iOSClient/Networking/NCConfigServer.swift @@ -1,25 +1,6 @@ -// -// NCConfigServer.swift -// Nextcloud -// -// Created by Marino Faggiana on 05/12/22. -// Copyright © 2022 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2022 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import UIKit diff --git a/iOSClient/Networking/NCNetworking+Download.swift b/iOSClient/Networking/NCNetworking+Download.swift index 22ecb91653..4d9fe0c51c 100644 --- a/iOSClient/Networking/NCNetworking+Download.swift +++ b/iOSClient/Networking/NCNetworking+Download.swift @@ -1,25 +1,6 @@ -// -// NCNetworking+Download.swift -// Nextcloud -// -// Created by Marino Faggiana on 07/02/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit import NextcloudKit @@ -346,40 +327,3 @@ extension NCNetworking { } } } - -class NCOperationDownload: ConcurrentOperation, @unchecked Sendable { - var metadata: tableMetadata - var selector: String - - init(metadata: tableMetadata, selector: String) { - self.metadata = tableMetadata.init(value: metadata) - self.selector = selector - } - - override func start() { - guard !isCancelled else { return self.finish() } - - metadata.session = NCNetworking.shared.sessionDownload - metadata.sessionError = "" - metadata.sessionSelector = selector - metadata.sessionTaskIdentifier = 0 - metadata.status = NCGlobal.shared.metadataStatusWaitDownload - -// let metadata = NCManageDatabase.shared.addMetadata(metadata) - -// NCNetworking.shared.download(metadata: metadata, withNotificationProgressTask: true) { -// } completion: { _, _ in -// self.finish() -// } - Task { - await download(withSelector: self.selector) - } - } - - private func download(withSelector selector: String = "") async { - await NCNetworking.shared.downloadFile(metadata: metadata) { _ in - self.finish() - } taskHandler: { _ in } - - } -} diff --git a/iOSClient/Networking/NCNetworking+LivePhoto.swift b/iOSClient/Networking/NCNetworking+LivePhoto.swift index 072b3b087f..e861b76b81 100644 --- a/iOSClient/Networking/NCNetworking+LivePhoto.swift +++ b/iOSClient/Networking/NCNetworking+LivePhoto.swift @@ -1,25 +1,6 @@ -// -// NCNetworking+LivePhoto.swift -// Nextcloud -// -// Created by Marino Faggiana on 07/02/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit import NextcloudKit diff --git a/iOSClient/Networking/NCNetworking+Task.swift b/iOSClient/Networking/NCNetworking+Task.swift index 1b6122252b..f28830b583 100644 --- a/iOSClient/Networking/NCNetworking+Task.swift +++ b/iOSClient/Networking/NCNetworking+Task.swift @@ -1,25 +1,6 @@ -// -// NCNetworking+Task.swift -// Nextcloud -// -// Created by Marino Faggiana on 24/08/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import UIKit diff --git a/iOSClient/Networking/NCNetworking+Upload.swift b/iOSClient/Networking/NCNetworking+Upload.swift index 6c985dbe7c..9467e9c0b7 100644 --- a/iOSClient/Networking/NCNetworking+Upload.swift +++ b/iOSClient/Networking/NCNetworking+Upload.swift @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + import UIKit import NextcloudKit import Alamofire @@ -41,22 +45,6 @@ extension NCNetworking { } taskHandler(task) } progressHandler: { progress in - Task { - guard await self.progressQuantizer.shouldEmit(serverUrlFileName: serverUrlFileName, fraction: progress.fractionCompleted) else { - return - } - - if let metadata { - await NCManageDatabase.shared.setMetadataProgress(ocId: metadata.ocId, progress: progress.fractionCompleted) - await self.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferProgressDidUpdate(progress: Float(progress.fractionCompleted), - totalBytes: progress.totalUnitCount, - totalBytesExpected: progress.completedUnitCount, - fileName: metadata.fileName, - serverUrl: metadata.serverUrl) - } - } - } progressHandler(progress.completedUnitCount, progress.totalUnitCount, progress.fractionCompleted) } @@ -390,7 +378,6 @@ extension NCNetworking { error: self.global.diagnosticProblemsUploadServerError) } } - await NCManageDatabase.shared.updateBadge() } // MARK: - diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 4d78a1844e..37629dadd1 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -1,25 +1,6 @@ -// -// NCNetworking+WebDAV.swift -// Nextcloud -// -// Created by Marino Faggiana on 07/02/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit import NextcloudKit @@ -484,50 +465,6 @@ extension NCNetworking { return .success } - - func renameMetadata(_ metadata: tableMetadata, - fileNameNew: String, - indexPath: IndexPath, - viewController: UIViewController?, - completion: @escaping (_ error: NKError) -> Void) { - - let permission = NCMetadataPermissions.permissionsContainsString(metadata.permissions, permissions: NCMetadataPermissions.permissionCanRename) - if (!metadata.permissions.isEmpty && permission == false) || - (metadata.status != global.metadataStatusNormal && metadata.status != global.metadataStatusWaitRename) { -// return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_no_permission_modify_file_")) -// let error = await NCNetworkingE2EERename().rename(metadata: metadata, fileNameNew: fileNameNew) -// DispatchQueue.main.async { completion(error) } - completion(NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_no_permission_modify_file_")) - } - - if metadata.isDirectoryE2EE { -#if !EXTENSION - if isOffline { -// return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) - completion(NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) - } - Task { - let error = await NCNetworkingE2EERename().rename(metadata: metadata, fileNameNew: fileNameNew) - if error != .success { -// NCContentPresenter().showError(error: error) - completion(error) - } - } -#endif - } else { - Task { - await self.transferDispatcher.notifyAllDelegatesAsync { delegate in - let status = self.global.metadataStatusWaitRename - await NCManageDatabase.shared.renameMetadata(fileNameNew: fileNameNew, ocId: metadata.ocId, status: status) - delegate.transferReloadData(serverUrl: metadata.serverUrl, status: status) - } - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterRenameFile, userInfo: ["serverUrl": metadata.serverUrl, "account": metadata.account, "error": NKError(errorCode: 0, errorDescription: ""), "ocId": metadata.ocId, "indexPath": indexPath]) - - } - - completion(NKError(errorCode: 0, errorDescription: "")) - } - } func renameFileOrFolder(metadata: tableMetadata) async -> NKError { let serverUrlFileNameSource = metadata.serverUrlFileName diff --git a/iOSClient/Networking/NCNetworking.swift b/iOSClient/Networking/NCNetworking.swift index 0c86cf84c8..c0684756d4 100644 --- a/iOSClient/Networking/NCNetworking.swift +++ b/iOSClient/Networking/NCNetworking.swift @@ -37,203 +37,6 @@ protocol NCTransferDelegate: AnyObject { serverUrl: String) } -extension NCTransferDelegate { - func transferChange(status: String, - account: String, - fileName: String, - serverUrl: String, - selector: String?, - ocId: String, - destination: String?, - error: NKError) {} - func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) {} - func transferProgressDidUpdate(progress: Float, - totalBytes: Int64, - totalBytesExpected: Int64, - fileName: String, - serverUrl: String) {} -} - -/// Actor-based delegate dispatcher using weak references. -actor NCTransferDelegateDispatcher { - // Weak reference collection of delegates - private var transferDelegates = NSHashTable.weakObjects() - - /// Adds a delegate safely. - func addDelegate(_ delegate: NCTransferDelegate) { - transferDelegates.add(delegate) - } - - /// Remove a delegate safely. - func removeDelegate(_ delegate: NCTransferDelegate) { - transferDelegates.remove(delegate) - } - - /// Notifies all delegates. - func notifyAllDelegates(_ block: (NCTransferDelegate) -> Void) { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - block(delegate) - } - } - - func notifyAllDelegatesAsync(_ block: @escaping (NCTransferDelegate) async -> Void) async { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - await block(delegate) - } - } - - /// Notifies the delegate for a specific scene. - func notifyDelegate(forScene sceneIdentifier: String, _ block: (NCTransferDelegate) -> Void) { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - if delegate.sceneIdentifier == sceneIdentifier { - block(delegate) - } - } - } - - /// Notifies matching and non-matching delegates for a specific scene. - func notifyDelegates(forScene sceneIdentifier: String, - matching: (NCTransferDelegate) -> Void, - others: (NCTransferDelegate) -> Void) { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - if delegate.sceneIdentifier == sceneIdentifier { - matching(delegate) - } else { - others(delegate) - } - } - } -} - -/// A thread-safe registry for tracking in-flight `URLSessionTask` instances. -/// -/// Each task is associated with a string identifier (`identifier`) that you define, -/// allowing you to check whether a request is already running, avoid duplicates, -/// and cancel all active tasks at once. The registry automatically removes -/// completed tasks via `cleanupCompleted()` to keep memory usage compact. -/// -/// Typical use cases: -/// - Ensure only one task per identifier is active at a time. -/// - Query whether a specific request is still running (`isReading`). -/// - Forcefully stop a specific request (`cancel`). -/// - Forcefully stop all tasks when leaving a screen (`cancelAll`). -actor NetworkingTasks { - private var active: [(identifier: String, task: URLSessionTask)] = [] - - /// Returns whether there is an in-flight task for the given URL. - /// - /// A task is considered in-flight if its `state` is `.running` or `.suspended`. - /// - Parameter identifier: The identifier to check. - /// - Returns: `true` if a matching in-flight task exists; otherwise `false`. - func isReading(identifier: String) -> Bool { - // Drop finished/canceling tasks globally - cleanup() - - return active.contains { - $0.identifier == identifier && ($0.task.state == .running || $0.task.state == .suspended) - } - } - - /// Tracks a newly created `URLSessionTask` for the given identifier. - /// - /// If a running entry for the same identifier exists, it is removed before appending the new one. - /// - Parameters: - /// - identifier: The identifier associated with the task. - /// - task: The `URLSessionTask` to track. - func track(identifier: String, task: URLSessionTask) { - // Drop finished/canceling tasks globally - cleanup() - - active.removeAll { - $0.identifier == identifier && $0.task.state == .running - } - active.append((identifier, task)) - nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .start, message: "Start task for identifier: \(identifier)", consoleOnly: true) - } - - /// create a Identifier - /// - func createIdentifier(account: String? = nil, path: String? = nil, name: String) -> String { - if let account, - let path { - return account + "_" + path + "_" + name - } else if let path { - return path + "_" + name - } else { - return name - } - } - - /// Cancels and removes all tasks associated with the given id. - /// - /// - Parameter identifier: The identifier whose tasks should be canceled. - func cancel(identifier: String) { - // Drop finished/canceling tasks globally - cleanup() - - for element in active where element.identifier == identifier { - element.task.cancel() - nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .cancel, message: "Cancel task for identifier: \(identifier)", consoleOnly: true) - } - active.removeAll { - $0.identifier == identifier - } - } - - /// Cancels all tracked `URLSessionTask` and clears the registry. - /// - /// Call this when leaving the page/screen or when the operation must be forcefully stopped. - func cancelAll() { - active.forEach { - $0.task.cancel() - nkLog(tag: NCGlobal.shared.logNetworkingTasks, emoji: .cancel, message: "Cancel task with identifier: \($0.identifier)", consoleOnly: true) - } - active.removeAll() - } - - /// Removes tasks that have completed from the registry. - /// - /// Useful to keep the in-memory list compact during long-running operations. - func cleanup() { - active.removeAll { - $0.task.state == .completed || $0.task.state == .canceling - } - } -} - -/// Quantizes per-task progress updates to integer percentages (0...100). -/// Each (serverUrlFileName) pair is tracked separately, so you get -/// at most one update per integer percent for each transfer. -actor ProgressQuantizer { - private var lastPercent: [String: Int] = [:] - - /// Returns `true` only when integer percent changes (or hits 100). - /// - /// - Parameters: - /// - serverUrlFileName: The name of the file being transferred. - /// - fraction: Progress fraction [0.0 ... 1.0]. - func shouldEmit(serverUrlFileName: String, fraction: Double) -> Bool { - let percent = min(max(Int((fraction * 100).rounded(.down)), 0), 100) - - let last = lastPercent[serverUrlFileName] ?? -1 - guard percent != last || percent == 100 else { - return false - } - - lastPercent[serverUrlFileName] = percent - return true - } - - /// Clears stored state for a finished transfer. - func clear(serverUrlFileName: String) { - lastPercent.removeValue(forKey: serverUrlFileName) - } -} - class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { static let shared = NCNetworking() @@ -291,7 +94,6 @@ class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { let downloadAvatarQueue = Queuer(name: "downloadAvatarQueue", maxConcurrentOperationCount: 10, qualityOfService: .default) let fileExistsQueue = Queuer(name: "fileExistsQueue", maxConcurrentOperationCount: 10, qualityOfService: .default) #endif - let downloadQueue = Queuer(name: "downloadQueue", maxConcurrentOperationCount: NCBrandOptions.shared.httpMaximumConnectionsPerHostInDownload, qualityOfService: .default) // MARK: - init diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index 9121911b0b..930a965320 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -271,6 +271,7 @@ actor NCNetworkingProcess { await startTimer(interval: minInterval) } } else { + // Remove upload asset await removeUploadedAssetsIfNeeded() // Set Live Photo diff --git a/iOSClient/Networking/NCService.swift b/iOSClient/Networking/NCService.swift index 2987e2543c..f6715693d3 100644 --- a/iOSClient/Networking/NCService.swift +++ b/iOSClient/Networking/NCService.swift @@ -1,25 +1,6 @@ -// -// NCService.swift -// Nextcloud -// -// Created by Marino Faggiana on 14/03/18. -// Copyright © 2018 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2018 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit @preconcurrency import NextcloudKit diff --git a/iOSClient/Settings/PrivacySettingsViewController.swift b/iOSClient/Settings/PrivacySettingsViewController.swift index e4497c92bc..782f3e76bb 100644 --- a/iOSClient/Settings/PrivacySettingsViewController.swift +++ b/iOSClient/Settings/PrivacySettingsViewController.swift @@ -13,7 +13,7 @@ import XLForm class PrivacySettingsViewController: XLFormViewController{ - @objc public var isShowSettingsButton: Bool = false + @objc public var isShowSettingsButton: Bool = false override func viewDidLoad() { super.viewDidLoad() @@ -36,6 +36,8 @@ class PrivacySettingsViewController: XLFormViewController{ initializeForm() } + + //MARK: XLForm func initializeForm() { @@ -56,6 +58,7 @@ class PrivacySettingsViewController: XLFormViewController{ section.footerTitle = NSLocalizedString("_privacy_settings_help_text_", comment: "") form.addFormSection(section) + //custom cell section = XLFormSectionDescriptor.formSection(withTitle: "") section.footerTitle = NSLocalizedString("_required_data_collection_help_text_", comment: "") @@ -64,6 +67,7 @@ class PrivacySettingsViewController: XLFormViewController{ XLFormViewController.cellClassesForRowDescriptorTypes()["RequiredDataCollectionCustomCellType"] = RequiredDataCollectionSwitch.self + row = XLFormRowDescriptor(tag: "ButtonDestinationFolder", rowType: "RequiredDataCollectionCustomCellType", title: "") row.cellConfig["requiredDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue @@ -78,6 +82,7 @@ class PrivacySettingsViewController: XLFormViewController{ XLFormViewController.cellClassesForRowDescriptorTypes()["AnalysisDataCollectionCustomCellType"] = AnalysisDataCollectionSwitch.self + row = XLFormRowDescriptor(tag: "AnalysisDataCollectionSwitch", rowType: "AnalysisDataCollectionCustomCellType", title: "") row.cellConfig["analysisDataCollectionSwitchControl.onTintColor"] = NCBrandColor.shared.brand row.cellConfig["cellLabel.textAlignment"] = NSTextAlignment.left.rawValue @@ -92,11 +97,13 @@ class PrivacySettingsViewController: XLFormViewController{ section.addFormRow(row) + XLFormViewController.cellClassesForRowDescriptorTypes()["SaveSettingsButton"] = SaveSettingsCustomButtonCell.self section = XLFormSectionDescriptor.formSection(withTitle: "") form.addFormSection(section) + row = XLFormRowDescriptor(tag: "SaveSettingsButton", rowType: "SaveSettingsButton", title: "") row.cellConfig["backgroundColor"] = UIColor.clear diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index febf4069db..7d20929728 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -126,8 +126,8 @@ "_error_parameter_schema_" = "Wrong parameters, impossible to continue"; "_comments_" = "Comments"; "_sharing_" = "Sharing"; -//"_details_" = "Share"; -"_details_" = "Details"; +"_details_" = "Share"; +//"_details_" = "Details"; "_sub_details_" = "Subscription Details"; "_subscriptions_" = "Subscriptions"; "_dark_mode_" = "Dark mode"; @@ -232,6 +232,8 @@ "_settings_account_request_" = "Request account at startup"; "_print_" = "Print"; "_alias_" = "Alias"; +//"_alias_placeholder_" = "Write alias"; +//"_alias_footer_" = "Give your accounts a descriptive name such as Home, Office, School…"; "_alias_placeholder_" = "Write the alias"; "_alias_footer_" = "Give your account names a descriptive name such as Home, Office, School …"; "_chunk_size_mb_" = "Chunk size in MB"; @@ -281,7 +283,7 @@ //TOPasscodeSettingsWarningLabel.m "_1_failed_passcode_attempt" = "1 Failed Passcode Attempt"; -"_failed_passcode_attempts" = "%d Failed Passcode Attempts"; +"_failed_passcode_attempts" = "%ld Failed Passcode Attempts"; /* Background of the file listing view */ "_use_as_background_" = "Use it as a background"; @@ -322,6 +324,7 @@ "_status_message_" = "Status message"; "_status_message_placehorder_" = "What is your status?"; "_online_status_" = "Online status"; +"_clear_status_message_" = "Clear status message"; "_set_status_message_" = "Set status message"; "_clear_status_message_after_" = "Clear status after"; @@ -384,8 +387,8 @@ "_username_" = "Username"; "_change_credentials_" = "Change your credentials"; "_wifi_only_" = "Only use Wi-Fi connection"; -"_settings_autoupload_" = "Auto upload photos"; -//"_settings_autoupload_" = "Auto upload"; +//"_settings_autoupload_" = "Auto upload photos"; +"_settings_autoupload_" = "Auto upload"; "_app_version_" = "Application version"; "_app_in_use_" = "Application in use"; "_contact_by_email_" = "Contact us by email"; @@ -482,6 +485,7 @@ "_autoupload_change_location_footer_" = "Change folder used for \"Automatic upload of camera photos\" (if the option is enabled)"; "_autoupload_not_select_home_" = "Select a folder"; "_autoupload_save_album_" = "Copy photo or video into the photo album"; +"_autoupload_description_" = "Choose whether new photos or videos will be automatically uploaded to your account."; "_autoupload_fullphotos_" = "Upload the whole camera roll"; "_start_autoupload_" = "Turn on auto upload"; "_stop_autoupload_" = "Turn off auto upload"; @@ -625,9 +629,11 @@ "_add_offline_" = "Add to offline"; "_remove_passcode_" = "Remove password protection"; "_protect_passcode_" = "Protect with password"; +"_remove_favorites_" = "Unfavorite"; +"_add_favorites_" = "Favorite"; "_share_" = "Share"; "_reload_" = "Reload"; -"_open_in_" = "Open in …"; +"_open_in_" = "Open with..."; //"Open in …"; "_open_" = "Open …"; "_remove_local_file_" = "Remove locally"; "_add_local_" = "Add to local storage"; @@ -714,7 +720,7 @@ "_status_upload_error_" = "Error, waiting to upload"; "_select_media_folder_" = "Set Media folder"; "_media_viewimage_show_" = "Show only images"; -"_media_viewvideo_show_" = "Show only video"; +"_media_viewvideo_show_" = "Show only videos"; "_media_show_all_" = "Show both"; "_media_by_created_date_" = "Sort by created date"; "_media_by_upload_date_" = "Sort by upload date"; @@ -735,10 +741,12 @@ "_share_link_" = "Share link"; //"_share_link_" = "Share link"; "_share_link_" = "Link"; +//"_share_link_" = "Share link"; "_share_link_button_" = "Send link to …"; "_share_link_name_" = "Link name"; "_password_" = "Password"; "_share_password_" = "Password protected link"; +"_share_password_must_be_at_least_6_chars" = "Password must be at least 6 characters."; "_share_expirationdate_" = "Set expiration date for link"; "_date_" = "Date"; "_share_title_" = "Share"; @@ -796,6 +804,42 @@ "_new_comment_" = "New comment …"; "_edit_comment_" = "Edit comment"; "_delete_comment_" = "Delete comment"; +"_share_read_only_" = "View only"; +"_share_reshare_allowed_" = "Resharing is allowed."; +"_share_reshare_not_allowed_" = "Resharing is not allowed."; +"_sharing_message_" = "You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration."; +"_create_link_" = "Create Link"; +"personal_share_by_mail" = "Personal share by mail"; +"_your_shares_" = "Your Shares"; +"_share_linklabel_" = "Link '%@'"; +"_share_link_folder_" = "Link to folder"; +"_share_link_file_" = "Link to file"; +"no_shares_created" = "No shares created yet."; +"_advance_permissions_" = "Advanced permissions"; +"_send_new_email_" = "Send new email"; +"_apply_changes_" = "Apply changes"; +"_send_share_" = "Send share"; +"_PERMISSIONS_" = "PERMISSIONS"; +"share_editing_message" = "There are no editing functions for files unless you share them with MagentaCLOUD users."; +"_file_drop_message_" = "With File drop, only uploading is allowed. Only you can see files and folders that have been uploaded."; +"_custom_link_label" = "Your custom link label"; +"_set_password_" = "Set password"; +"_share_expiration_date_placeholder_"= "Expiration date for this share"; +"_share_download_limit_" = "Download Limit"; +"_share_download_limit_placeholder_" = "Enter download limit"; +"_share_download_limit_alert_empty_" = "Download limit cannot be empty."; +"_share_download_limit_alert_zero_" = "Download limit should be greater than 0."; +"_share_download_limit_alert_invalid_" = "Please enter a valid number for the download limit."; +"_share_remaining_download_" = "Downloads:"; +"_share_read_only_" = "View only"; +"_share_remaining_download_" = "Downloads:"; +"_share_editing_" = "Can edit"; +"_share_file_drop_" = "Filedrop only"; +"_share_hide_download_" = "Prevent download"; +//"_share_note_recipient_" = "YOUR MESSAGE"; +"_share_note_recipient_" = "Note to recipient"; +"_shareLinksearch_placeholder_" = "Contact name or email address"; +"_shareLinksearch_mail_placeholder_" = "Type a name or an email and press Enter"; "_share_reshare_allowed_" = "Resharing is allowed."; "_share_reshare_not_allowed_" = "Resharing is not allowed."; "_sharing_message_" = "You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration."; @@ -865,6 +909,7 @@ "_share_via_link_menu_password_label_" = "Password protect (%1$s)"; "_share_link_empty_exp_date_" = "You must select expiration date."; "_share_link_empty_note_message_" = "Please enter note."; +"_share_permission_should_not_be_empty_" = "Please select at least one permission."; "_remote_" = "Remote"; "_remote_group_" = "Remote group"; "_conversation_" = "Conversation"; @@ -1098,7 +1143,7 @@ "_voice_memo_filename_" = "Voice memo"; "_voice_memo_title_" = "Upload voice memo"; "_too_many_failed_passcode_attempts_error_" = "Too many failed passcode attempts. Please try again."; -"Passcodes didn't match. Try again." = "Passcodes didn't match. Try again."; +//"Passcodes didn't match. Try again." = "Passcodes didn't match. Try again."; "Delete" = "Delete"; "Cancel" = "Cancel"; "_certificate_details_" = "Certificate Server details"; @@ -1157,7 +1202,7 @@ "_waiting_for_" = "Waiting for:"; "_waiting_" = "Waiting …"; "_reachable_wifi_" = "network reachable via Wi-Fi or cable"; -"_ITMS-90076_" = "Due to a change in the Nextcloud application identifier, the settings and password for accessing your cloud are reset, so please re-enter your account data and check your Settings. We are sorry about that."; +"_ITMS-90076_" = "Due to a change in the MagentaCLOUD application identifier, the settings and password for accessing your cloud are reset, so please re-enter your account data and check your Settings. We are sorry about that."; "_password_not_present_" = "Please re-insert your credentials."; "_copy_passphrase_" = "Copy passphrase"; "_ok_copy_passphrase_" = "OK and copy passphrase"; @@ -1315,7 +1360,7 @@ "_delete_in_progress_" = "Delete in progress …"; "_download_in_progress_" = "Download in progress …"; "_upload_in_progress_" = "Upload in progress …"; -"_transfer_in_progress_" = "Transfer in progress …"; +"_transfer_in_progress_" = "Transfer in progress …"; "_in_waiting_" = "In waiting"; "_in_progress_" = "In progress"; "_in_error_" = "In error"; @@ -1530,3 +1575,46 @@ You can stop it at any time, adjust the settings, and enable it again."; "_e2ee_setup_store_privatekey_" = "Private key generation returned no usable key"; "_e2ee_setup_verify_publickey_" = "Public key does not match the certificate"; "_e2ee_setup_generate_passphrase_" = "Unable to generate a passphrase"; + +// MARK: Albums +"_albums_loading_popup_desc_" = "Wait a moment..."; +"_albums_list_nav_title_" = "Albums"; +"_albums_list_new_album_btn_" = "New"; +"_albums_list_loading_msg_" = "Loading..."; +"_albums_list_error_msg_" = "Unable to load albums. Please try again later!"; +"_albums_list_album_name_validation_nonempty_" = "Album name cannot be empty."; +"_albums_list_album_name_validation_min_length_" = "Album name must be at least 3 characters."; +"_albums_list_album_name_validation_max_length_" = "Album name cannot be more than 30 characters."; +"_albums_list_album_name_validation_specials_" = "Album name cannot contain slashes."; +"_albums_list_empty_heading_" = "Create\nAlbums\nfor your\nPhotos"; +"_albums_list_empty_subheading_" = "You can organize all your photos in as many albums as you like. You haven't created an album yet."; +"_albums_list_empty_new_album_btn_" = "Create album"; +"_albums_list_own_albums_heading_" = "My albums"; +"_albums_list_entities_" = "Items"; +"_albums_list_new_album_popup_title_" = "Create new Album"; +"_albums_list_new_album_popup_desc_" = "Please enter an album name between 3 and 30 characters."; +"_albums_list_new_album_popup_hint_" = "Album's name"; +"_albums_list_new_album_popup_positive_btn_" = "Create"; +"_albums_list_new_album_popup_negative_btn_" = "Cancel"; +"_albums_list_rename_album_popup_title_" = "Rename Album"; +"_albums_list_rename_album_popup_desc_" = "Please enter new album name between 3 and 30 characters."; +"_albums_list_rename_album_popup_hint_" = "Album's new name"; +"_albums_list_rename_album_popup_positive_btn_" = "Rename"; +"_albums_list_rename_album_popup_negative_btn_" = "Cancel"; +"_albums_photo_selection_sheet_title_" = "Select items"; +"_albums_photo_selection_sheet_back_btn_" = "Back"; +"_albums_photo_selection_sheet_done_btn_" = "Done"; +"_albums_photos_loading_msg_" = "Loading photos..."; +"_albums_photos_error_msg_" = "Unable to load photos. Please try again later!"; +"_albums_photos_empty_heading_" = "All that's\nmissing are\nyour photos"; +"_albums_photos_empty_subheading_" = "You can add as many photos as you like. A photo can also belong to more than one album."; +"_albums_photos_empty_add_photos_btn_" = "Add photos"; +"_albums_photos_add_photos_btn_" = "Add"; +"_albums_photos_rename_album_btn_" = "Rename Album"; +"_albums_photos_delete_album_btn_" = "Delete Album"; +"_albums_delete_album_popup_title_" = "Delete Album?"; +"_albums_delete_album_popup_desc_" = "Are you sure you want to delete this album? This action cannot be undone."; +"_albums_delete_album_popup_positive_btn_" = "Delete"; +"_albums_delete_album_popup_negative_btn_" = "Cancel"; +"_add_to_album" = "Add to Album"; +"_remove_from_album_" = "Remove from Album";