diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 17445d9..3380add 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -71,6 +71,13 @@ struct LCAppBanner : View { Capsule().fill(Color("JITBadgeColor")) ) } + if model.uiIsLocked && !model.uiIsHidden { + Text("lc.appBanner.locked".loc).font(.system(size: 8)).bold().padding(2) + .frame(width: 50, height:16) + .background( + Capsule().fill(Color("BadgeColor")) + ) + } } Text("\(appInfo.version()) - \(appInfo.bundleIdentifier())").font(.system(size: 12)).foregroundColor(Color("FontColor")) @@ -239,6 +246,18 @@ struct LCAppBanner : View { } func runApp() async { + if appInfo.isLocked && !sharedModel.isHiddenAppUnlocked { + do { + if !(try await LCUtils.authenticateUser()) { + return + } + } catch { + errorInfo = error.localizedDescription + errorShow = true + return + } + } + do { try await model.runApp() } catch { @@ -319,3 +338,37 @@ struct LCAppBanner : View { } + + +struct LCAppSkeletonBanner: View { + var body: some View { + HStack { + RoundedRectangle(cornerRadius: 12) + .fill(Color.gray.opacity(0.3)) + .frame(width: 60, height: 60) + + VStack(alignment: .leading, spacing: 5) { + RoundedRectangle(cornerRadius: 4) + .fill(Color.gray.opacity(0.3)) + .frame(width: 100, height: 16) + + RoundedRectangle(cornerRadius: 4) + .fill(Color.gray.opacity(0.3)) + .frame(width: 150, height: 12) + + RoundedRectangle(cornerRadius: 4) + .fill(Color.gray.opacity(0.3)) + .frame(width: 120, height: 8) + } + + Spacer() + + RoundedRectangle(cornerRadius: 16) + .fill(Color.gray.opacity(0.3)) + .frame(width: 70, height: 32) + } + .padding() + .frame(height: 88) + .background(RoundedRectangle(cornerRadius: 22).fill(Color.gray.opacity(0.1))) + } +} diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index 17ee1e5..0136f55 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -48,7 +48,7 @@ struct LCAppListView : View, LCAppBannerDelegate, LCAppModelDelegate { @State private var isNavigationActive = false @EnvironmentObject private var sharedModel : SharedModel - + init(apps: Binding<[LCAppModel]>, hiddenApps: Binding<[LCAppModel]>, appDataFolderNames: Binding<[String]>, tweakFolderNames: Binding<[String]>) { _installOptions = State(initialValue: []) _apps = apps @@ -95,38 +95,61 @@ struct LCAppListView : View, LCAppBannerDelegate, LCAppModelDelegate { } .padding() .animation(.easeInOut, value: apps) - - if !sharedModel.isHiddenAppUnlocked { - Text(apps.count > 0 ? "lc.appList.appCounter %lld".localizeWithFormat(apps.count) : "lc.appList.installTip".loc).foregroundStyle(.gray) - .onTapGesture(count: 3) { - Task { await authenticateUser() } - } - } - - if sharedModel.isHiddenAppUnlocked { - LazyVStack { - HStack { - Text("lc.appList.hiddenApps".loc) - .font(.system(.title2).bold()) - .border(Color.black) - Spacer() + VStack { + if LCUtils.appGroupUserDefault.bool(forKey: "LCStrictHiding") { + if sharedModel.isHiddenAppUnlocked { + LazyVStack { + HStack { + Text("lc.appList.hiddenApps".loc) + .font(.system(.title2).bold()) + Spacer() + } + ForEach(hiddenApps, id: \.self) { app in + LCAppBanner(appModel: app, delegate: self, appDataFolders: $appDataFolderNames, tweakFolders: $tweakFolderNames) + } + } + .padding() + .transition(.opacity) + .animation(.easeInOut, value: apps) + + if hiddenApps.count == 0 { + Text("lc.appList.hideAppTip".loc) + .foregroundStyle(.gray) + } } - ForEach(hiddenApps, id: \.self) { app in - LCAppBanner(appModel: app, delegate: self, appDataFolders: $appDataFolderNames, tweakFolders: $tweakFolderNames) + } else if hiddenApps.count > 0 { + LazyVStack { + HStack { + Text("lc.appList.hiddenApps".loc) + .font(.system(.title2).bold()) + Spacer() + } + ForEach(hiddenApps, id: \.self) { app in + if sharedModel.isHiddenAppUnlocked { + LCAppBanner(appModel: app, delegate: self, appDataFolders: $appDataFolderNames, tweakFolders: $tweakFolderNames) + } else { + LCAppSkeletonBanner() + } + } + .animation(.easeInOut, value: sharedModel.isHiddenAppUnlocked) + .onTapGesture { + Task { await authenticateUser() } + } } - .transition(.scale) + .padding() + .animation(.easeInOut, value: apps) } - .padding() - .animation(.easeInOut, value: apps) - - if hiddenApps.count == 0 { - Text("lc.appList.hideAppTip".loc) - .foregroundStyle(.gray) - } - Text(apps.count + hiddenApps.count > 0 ? "lc.appList.appCounter %lld".localizeWithFormat(apps.count + hiddenApps.count) : "lc.appList.installTip".loc).foregroundStyle(.gray) - } - + + let appCount = sharedModel.isHiddenAppUnlocked ? apps.count + hiddenApps.count : apps.count + Text(appCount > 0 ? "lc.appList.appCounter %lld".localizeWithFormat(appCount) : "lc.appList.installTip".loc) + .foregroundStyle(.gray) + .animation(.easeInOut, value: appCount) + .onTapGesture(count: 3) { + Task { await authenticateUser() } + } + }.animation(.easeInOut, value: LCUtils.appGroupUserDefault.bool(forKey: "LCStrictHiding")) + if LCUtils.multiLCStatus == 2 { Text("lc.appList.manageInPrimaryTip".loc).foregroundStyle(.gray).padding() } @@ -272,7 +295,7 @@ struct LCAppListView : View, LCAppBannerDelegate, LCAppModelDelegate { return } - if appToLaunch.appInfo.isHidden && !sharedModel.isHiddenAppUnlocked { + if appToLaunch.appInfo.isLocked && !sharedModel.isHiddenAppUnlocked { do { if !(try await LCUtils.authenticateUser()) { return @@ -460,10 +483,13 @@ struct LCAppListView : View, LCAppBannerDelegate, LCAppModelDelegate { return } var appFound : LCAppModel? = nil - var isFoundAppHidden = false + var isFoundAppLocked = false for app in apps { if app.appInfo.relativeBundlePath == bundleId { appFound = app + if app.appInfo.isLocked { + isFoundAppLocked = true + } break } } @@ -471,13 +497,13 @@ struct LCAppListView : View, LCAppBannerDelegate, LCAppModelDelegate { for app in hiddenApps { if app.appInfo.relativeBundlePath == bundleId { appFound = app - isFoundAppHidden = true + isFoundAppLocked = true break } } } - if isFoundAppHidden && !sharedModel.isHiddenAppUnlocked { + if isFoundAppLocked && !sharedModel.isHiddenAppUnlocked { do { let result = try await LCUtils.authenticateUser() if !result { diff --git a/LiveContainerSwiftUI/LCAppModel.swift b/LiveContainerSwiftUI/LCAppModel.swift index f24f815..dbe4401 100644 --- a/LiveContainerSwiftUI/LCAppModel.swift +++ b/LiveContainerSwiftUI/LCAppModel.swift @@ -16,6 +16,7 @@ class LCAppModel: ObservableObject, Hashable { @Published var uiIsJITNeeded : Bool @Published var uiIsHidden : Bool + @Published var uiIsLocked : Bool @Published var uiIsShared : Bool @Published var uiDataFolder : String? @Published var uiTweakFolder : String? @@ -29,9 +30,14 @@ class LCAppModel: ObservableObject, Hashable { init(appInfo : LCAppInfo, delegate: LCAppModelDelegate? = nil) { self.appInfo = appInfo self.delegate = delegate + + if !appInfo.isLocked && appInfo.isHidden { + appInfo.isLocked = true + } self.uiIsJITNeeded = appInfo.isJITNeeded self.uiIsHidden = appInfo.isHidden + self.uiIsLocked = appInfo.isLocked self.uiIsShared = appInfo.isShared self.uiDataFolder = appInfo.getDataUUIDNoAssign() self.uiTweakFolder = appInfo.tweakFolder() @@ -121,6 +127,20 @@ class LCAppModel: ObservableObject, Hashable { LCUtils.launchToGuestApp() } + + func toggleLock() async { + if appInfo.isLocked { + appInfo.isLocked = false + uiIsLocked = false + + if appInfo.isHidden { + await toggleHidden() + } + } else { + appInfo.isLocked = true + uiIsLocked = true + } + } func toggleHidden() async { delegate?.closeNavigationView() diff --git a/LiveContainerSwiftUI/LCAppSettingsView.swift b/LiveContainerSwiftUI/LCAppSettingsView.swift index 13d2f53..42adc93 100644 --- a/LiveContainerSwiftUI/LCAppSettingsView.swift +++ b/LiveContainerSwiftUI/LCAppSettingsView.swift @@ -153,20 +153,45 @@ struct LCAppSettingsView : View{ } footer: { Text("lc.appSettings.launchWithJitDesc".loc) } - - if sharedModel.isHiddenAppUnlocked { - Section { + + Section { + Toggle(isOn: $model.uiIsLocked) { + Text("lc.appSettings.lockApp".loc) + } + .onChange(of: model.uiIsLocked, perform: { newValue in + Task { + if !newValue { + do { + let result = try await LCUtils.authenticateUser() + if !result { + model.uiIsLocked = true + return + } + } catch { + return + } + } + + await model.toggleLock() + } + }) + + if model.uiIsLocked { Toggle(isOn: $model.uiIsHidden) { Text("lc.appSettings.hideApp".loc) } - .onChange(of: model.uiIsHidden, perform: { newValue in + .onChange(of: model.uiIsHidden, perform: { _ in Task { await toggleHidden() } }) - } footer: { + .transition(.opacity.combined(with: .slide)) + } + } footer: { + if model.uiIsLocked { Text("lc.appSettings.hideAppDesc".loc) + .transition(.opacity.combined(with: .slide)) } - } + Section { Toggle(isOn: $model.uiDoSymlinkInbox) { diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift index 29c96e8..2f404db 100644 --- a/LiveContainerSwiftUI/LCSettingsView.swift +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -157,14 +157,12 @@ struct LCSettingsView: View { Text("lc.settings.injectLCItselfDesc".loc) } - if sharedModel.isHiddenAppUnlocked { - Section { - Toggle(isOn: $strictHiding) { - Text("lc.settings.strictHiding".loc) - } - } footer: { - Text("lc.settings.strictHidingDesc".loc) + Section { + Toggle(isOn: $strictHiding) { + Text("lc.settings.strictHiding".loc) } + } footer: { + Text("lc.settings.strictHidingDesc".loc) } Section { diff --git a/LiveContainerSwiftUI/LCWebView.swift b/LiveContainerSwiftUI/LCWebView.swift index 85cb57c..09f5a18 100644 --- a/LiveContainerSwiftUI/LCWebView.swift +++ b/LiveContainerSwiftUI/LCWebView.swift @@ -169,7 +169,7 @@ struct LCWebView: View { return } - if appToLaunch.appInfo.isHidden && !sharedModel.isHiddenAppUnlocked { + if appToLaunch.appInfo.isLocked && !sharedModel.isHiddenAppUnlocked { do { if !(try await LCUtils.authenticateUser()) { @@ -214,7 +214,7 @@ struct LCWebView: View { return } - if appToLaunch.appInfo.isHidden && !sharedModel.isHiddenAppUnlocked { + if appToLaunch.appInfo.isLocked && !sharedModel.isHiddenAppUnlocked { do { if !(try await LCUtils.authenticateUser()) { return diff --git a/LiveContainerSwiftUI/Localizable.xcstrings b/LiveContainerSwiftUI/Localizable.xcstrings index 4a8a083..83f643f 100644 --- a/LiveContainerSwiftUI/Localizable.xcstrings +++ b/LiveContainerSwiftUI/Localizable.xcstrings @@ -222,6 +222,23 @@ } } }, + "lc.appBanner.locked" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "LOCKED" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "锁定" + } + } + } + }, "lc.appBanner.uninstall" : { "extractionState" : "manual", "localizations" : { @@ -790,6 +807,23 @@ } } }, + "lc.appSettings.lockApp" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lock App" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "锁定App" + } + } + } + }, "lc.appSettings.hideApp" : { "extractionState" : "manual", "localizations" : { diff --git a/LiveContainerUI/LCAppInfo.h b/LiveContainerUI/LCAppInfo.h index 329f241..f64e909 100644 --- a/LiveContainerUI/LCAppInfo.h +++ b/LiveContainerUI/LCAppInfo.h @@ -8,6 +8,7 @@ @property NSString* relativeBundlePath; @property bool isShared; @property bool isJITNeeded; +@property bool isLocked; @property bool isHidden; @property bool doSymlinkInbox; @property bool bypassAssertBarrierOnQueue; diff --git a/LiveContainerUI/LCAppInfo.m b/LiveContainerUI/LCAppInfo.m index e88b306..ea31f82 100644 --- a/LiveContainerUI/LCAppInfo.m +++ b/LiveContainerUI/LCAppInfo.m @@ -9,7 +9,7 @@ @implementation LCAppInfo - (instancetype)initWithBundlePath:(NSString*)bundlePath { self = [super init]; self.isShared = false; - if(self) { + if(self) { _bundlePath = bundlePath; _info = [NSMutableDictionary dictionaryWithContentsOfFile:[NSString stringWithFormat:@"%@/Info.plist", bundlePath]]; @@ -293,6 +293,19 @@ - (void)setIsJITNeeded:(bool)isJITNeeded { } +- (bool)isLocked { + if(_info[@"isLocked"] != nil) { + return [_info[@"isLocked"] boolValue]; + } else { + return NO; + } +} +- (void)setIsLocked:(bool)isLocked { + _info[@"isLocked"] = [NSNumber numberWithBool:isLocked]; + [self save]; + +} + - (bool)isHidden { if(_info[@"isHidden"] != nil) { return [_info[@"isHidden"] boolValue]; diff --git a/TweakLoader/UIKit+GuestHooks.m b/TweakLoader/UIKit+GuestHooks.m index 678ef0a..6a590ea 100644 --- a/TweakLoader/UIKit+GuestHooks.m +++ b/TweakLoader/UIKit+GuestHooks.m @@ -174,7 +174,7 @@ void handleLiveContainerLaunch(NSURL* url) { NSBundle* bundle = [NSClassFromString(@"LCSharedUtils") findBundleWithBundleId: bundleName]; if(!bundle || ([bundle.infoDictionary[@"isHidden"] boolValue] && [NSUserDefaults.lcSharedDefaults boolForKey:@"LCStrictHiding"])) { LCShowAppNotFoundAlert(bundleName); - } else if ([bundle.infoDictionary[@"isHidden"] boolValue]) { + } else if ([bundle.infoDictionary[@"isLocked"] boolValue]) { // need authentication authenticateUser(^(BOOL success, NSError *error) { if (success) {