瀏覽代碼

UI: Rewrite profile system to enable user-provided storage location

This change enables loading profiles from locations different than
OBS' own configuration directory.

It also rewrites profile management in the app to work off an in-memory
collection of profiles found on disk and does not require iterating
over directory contents for most profile interactions by the app.
PatTheMav 1 年之前
父節點
當前提交
607d37b423

+ 272 - 308
UI/api-interface.cpp

@@ -16,7 +16,6 @@ template<typename T> static T GetOBSRef(QListWidgetItem *item)
 	return item->data(static_cast<int>(QtDataRole::OBSRef)).value<T>();
 }
 
-void EnumProfiles(function<bool(const char *, const char *)> &&cb);
 void EnumSceneCollections(function<bool(const char *, const char *)> &&cb);
 
 extern volatile bool streaming_active;
@@ -218,29 +217,24 @@ struct OBSStudioAPI : obs_frontend_callbacks {
 	void
 	obs_frontend_get_profiles(std::vector<std::string> &strings) override
 	{
-		auto addProfile = [&](const char *name, const char *) {
-			strings.emplace_back(name);
-			return true;
-		};
+		const OBSProfileCache &profiles = main->GetProfileCache();
 
-		EnumProfiles(addProfile);
+		for (auto &[profileName, profile] : profiles) {
+			strings.emplace_back(profileName);
+		}
 	}
 
 	char *obs_frontend_get_current_profile(void) override
 	{
-		const char *name = config_get_string(App()->GlobalConfig(),
-						     "Basic", "Profile");
-		return bstrdup(name);
+		const OBSProfile &profile = main->GetCurrentProfile();
+		return bstrdup(profile.name.c_str());
 	}
 
 	char *obs_frontend_get_current_profile_path(void) override
 	{
-		char profilePath[512];
-		int ret = GetProfilePath(profilePath, sizeof(profilePath), "");
-		if (ret <= 0)
-			return nullptr;
+		const OBSProfile &profile = main->GetCurrentProfile();
 
-		return bstrdup(profilePath);
+		return bstrdup(profile.path.u8string().c_str());
 	}
 
 	void obs_frontend_set_current_profile(const char *profile) override
@@ -510,353 +504,323 @@ struct OBSStudioAPI : obs_frontend_callbacks {
 
 	config_t *obs_frontend_get_profile_config(void) override
 	{
-		return main->basicConfig;
-		config_t *obs_frontend_get_global_config(void) override
-		{
-			blog(LOG_WARNING,
-			     "DEPRECATION: obs_frontend_get_global_config is deprecated. Read from global or user configuration explicitly instead.");
-			return App()->GetAppConfig();
-		}
-
-		config_t *obs_frontend_get_app_config(void) override
-		{
-			return App()->GetAppConfig();
-		}
+		return main->activeConfiguration;
+	}
 
-		config_t *obs_frontend_get_user_config(void) override
-		{
-			return App()->GetUserConfig();
-		}
+	config_t *obs_frontend_get_global_config(void) override
+	{
+		blog(LOG_WARNING,
+		     "DEPRECATION: obs_frontend_get_global_config is deprecated. Read from global or user configuration explicitly instead.");
+		return App()->GetAppConfig();
+	}
 
-		void obs_frontend_open_projector(const char *type, int monitor,
-						 const char *geometry,
-						 const char *name) override
-		{
-			SavedProjectorInfo proj = {
-				ProjectorType::Preview,
-				monitor,
-				geometry ? geometry : "",
-				name ? name : "",
-			};
-			if (type) {
-				if (astrcmpi(type, "Source") == 0)
-					proj.type = ProjectorType::Source;
-				else if (astrcmpi(type, "Scene") == 0)
-					proj.type = ProjectorType::Scene;
-				else if (astrcmpi(type, "StudioProgram") == 0)
-					proj.type =
-						ProjectorType::StudioProgram;
-				else if (astrcmpi(type, "Multiview") == 0)
-					proj.type = ProjectorType::Multiview;
-			}
-			QMetaObject::invokeMethod(
-				main, "OpenSavedProjector", WaitConnection(),
-				Q_ARG(SavedProjectorInfo *, &proj));
-		}
+	config_t *obs_frontend_get_app_config(void) override
+	{
+		return App()->GetAppConfig();
+	}
 
-		void obs_frontend_save(void) override
-		{
-			main->SaveProject();
-		}
+	config_t *obs_frontend_get_user_config(void) override
+	{
+		return App()->GetUserConfig();
+	}
 
-		void obs_frontend_defer_save_begin(void) override
-		{
-			QMetaObject::invokeMethod(main, "DeferSaveBegin");
-		}
+	void obs_frontend_open_projector(const char *type, int monitor,
+					 const char *geometry,
+					 const char *name) override
+	{
+		SavedProjectorInfo proj = {
+			ProjectorType::Preview,
+			monitor,
+			geometry ? geometry : "",
+			name ? name : "",
+		};
+		if (type) {
+			if (astrcmpi(type, "Source") == 0)
+				proj.type = ProjectorType::Source;
+			else if (astrcmpi(type, "Scene") == 0)
+				proj.type = ProjectorType::Scene;
+			else if (astrcmpi(type, "StudioProgram") == 0)
+				proj.type = ProjectorType::StudioProgram;
+			else if (astrcmpi(type, "Multiview") == 0)
+				proj.type = ProjectorType::Multiview;
+		}
+		QMetaObject::invokeMethod(main, "OpenSavedProjector",
+					  WaitConnection(),
+					  Q_ARG(SavedProjectorInfo *, &proj));
+	}
 
-		void obs_frontend_defer_save_end(void) override
-		{
-			QMetaObject::invokeMethod(main, "DeferSaveEnd");
-		}
+	void obs_frontend_save(void) override { main->SaveProject(); }
 
-		void obs_frontend_add_save_callback(
-			obs_frontend_save_cb callback, void *private_data)
-			override
-		{
-			size_t idx = GetCallbackIdx(saveCallbacks, callback,
-						    private_data);
-			if (idx == (size_t)-1)
-				saveCallbacks.emplace_back(callback,
-							   private_data);
-		}
+	void obs_frontend_defer_save_begin(void) override
+	{
+		QMetaObject::invokeMethod(main, "DeferSaveBegin");
+	}
 
-		void obs_frontend_remove_save_callback(
-			obs_frontend_save_cb callback, void *private_data)
-			override
-		{
-			size_t idx = GetCallbackIdx(saveCallbacks, callback,
-						    private_data);
-			if (idx == (size_t)-1)
-				return;
+	void obs_frontend_defer_save_end(void) override
+	{
+		QMetaObject::invokeMethod(main, "DeferSaveEnd");
+	}
 
-			saveCallbacks.erase(saveCallbacks.begin() + idx);
-		}
+	void obs_frontend_add_save_callback(obs_frontend_save_cb callback,
+					    void *private_data) override
+	{
+		size_t idx =
+			GetCallbackIdx(saveCallbacks, callback, private_data);
+		if (idx == (size_t)-1)
+			saveCallbacks.emplace_back(callback, private_data);
+	}
 
-		void obs_frontend_add_preload_callback(
-			obs_frontend_save_cb callback, void *private_data)
-			override
-		{
-			size_t idx = GetCallbackIdx(preloadCallbacks, callback,
-						    private_data);
-			if (idx == (size_t)-1)
-				preloadCallbacks.emplace_back(callback,
-							      private_data);
-		}
+	void obs_frontend_remove_save_callback(obs_frontend_save_cb callback,
+					       void *private_data) override
+	{
+		size_t idx =
+			GetCallbackIdx(saveCallbacks, callback, private_data);
+		if (idx == (size_t)-1)
+			return;
 
-		void obs_frontend_remove_preload_callback(
-			obs_frontend_save_cb callback, void *private_data)
-			override
-		{
-			size_t idx = GetCallbackIdx(preloadCallbacks, callback,
-						    private_data);
-			if (idx == (size_t)-1)
-				return;
+		saveCallbacks.erase(saveCallbacks.begin() + idx);
+	}
 
-			preloadCallbacks.erase(preloadCallbacks.begin() + idx);
-		}
+	void obs_frontend_add_preload_callback(obs_frontend_save_cb callback,
+					       void *private_data) override
+	{
+		size_t idx = GetCallbackIdx(preloadCallbacks, callback,
+					    private_data);
+		if (idx == (size_t)-1)
+			preloadCallbacks.emplace_back(callback, private_data);
+	}
 
-		void obs_frontend_push_ui_translation(
-			obs_frontend_translate_ui_cb translate) override
-		{
-			App()->PushUITranslation(translate);
-		}
+	void obs_frontend_remove_preload_callback(obs_frontend_save_cb callback,
+						  void *private_data) override
+	{
+		size_t idx = GetCallbackIdx(preloadCallbacks, callback,
+					    private_data);
+		if (idx == (size_t)-1)
+			return;
 
-		void obs_frontend_pop_ui_translation(void) override
-		{
-			App()->PopUITranslation();
-		}
+		preloadCallbacks.erase(preloadCallbacks.begin() + idx);
+	}
 
-		void obs_frontend_set_streaming_service(obs_service_t * service)
-			override
-		{
-			main->SetService(service);
-		}
+	void obs_frontend_push_ui_translation(
+		obs_frontend_translate_ui_cb translate) override
+	{
+		App()->PushUITranslation(translate);
+	}
 
-		obs_service_t *obs_frontend_get_streaming_service(void) override
-		{
-			return main->GetService();
-		}
+	void obs_frontend_pop_ui_translation(void) override
+	{
+		App()->PopUITranslation();
+	}
 
-		void obs_frontend_save_streaming_service(void) override
-		{
-			main->SaveService();
-		}
+	void obs_frontend_set_streaming_service(obs_service_t *service) override
+	{
+		main->SetService(service);
+	}
 
-		bool obs_frontend_preview_program_mode_active(void) override
-		{
-			return main->IsPreviewProgramMode();
-		}
+	obs_service_t *obs_frontend_get_streaming_service(void) override
+	{
+		return main->GetService();
+	}
 
-		void obs_frontend_set_preview_program_mode(bool enable) override
-		{
-			main->SetPreviewProgramMode(enable);
-		}
+	void obs_frontend_save_streaming_service(void) override
+	{
+		main->SaveService();
+	}
 
-		void obs_frontend_preview_program_trigger_transition(void)
-			override
-		{
-			QMetaObject::invokeMethod(main, "TransitionClicked");
-		}
+	bool obs_frontend_preview_program_mode_active(void) override
+	{
+		return main->IsPreviewProgramMode();
+	}
 
-		bool obs_frontend_preview_enabled(void) override
-		{
-			return main->previewEnabled;
-		}
+	void obs_frontend_set_preview_program_mode(bool enable) override
+	{
+		main->SetPreviewProgramMode(enable);
+	}
 
-		void obs_frontend_set_preview_enabled(bool enable) override
-		{
-			if (main->previewEnabled != enable)
-				main->EnablePreviewDisplay(enable);
-		}
+	void obs_frontend_preview_program_trigger_transition(void) override
+	{
+		QMetaObject::invokeMethod(main, "TransitionClicked");
+	}
 
-		obs_source_t *obs_frontend_get_current_preview_scene(void)
-			override
-		{
-			if (main->IsPreviewProgramMode()) {
-				OBSSource source =
-					main->GetCurrentSceneSource();
-				return obs_source_get_ref(source);
-			}
+	bool obs_frontend_preview_enabled(void) override
+	{
+		return main->previewEnabled;
+	}
 
-			return nullptr;
-		}
+	void obs_frontend_set_preview_enabled(bool enable) override
+	{
+		if (main->previewEnabled != enable)
+			main->EnablePreviewDisplay(enable);
+	}
 
-		void obs_frontend_set_current_preview_scene(obs_source_t *
-							    scene) override
-		{
-			if (main->IsPreviewProgramMode()) {
-				QMetaObject::invokeMethod(
-					main, "SetCurrentScene",
-					Q_ARG(OBSSource, OBSSource(scene)),
-					Q_ARG(bool, false));
-			}
+	obs_source_t *obs_frontend_get_current_preview_scene(void) override
+	{
+		if (main->IsPreviewProgramMode()) {
+			OBSSource source = main->GetCurrentSceneSource();
+			return obs_source_get_ref(source);
 		}
 
-		void obs_frontend_take_screenshot(void) override
-		{
-			QMetaObject::invokeMethod(main, "Screenshot");
-		}
+		return nullptr;
+	}
 
-		void obs_frontend_take_source_screenshot(obs_source_t * source)
-			override
-		{
-			QMetaObject::invokeMethod(main, "Screenshot",
+	void
+	obs_frontend_set_current_preview_scene(obs_source_t *scene) override
+	{
+		if (main->IsPreviewProgramMode()) {
+			QMetaObject::invokeMethod(main, "SetCurrentScene",
 						  Q_ARG(OBSSource,
-							OBSSource(source)));
+							OBSSource(scene)),
+						  Q_ARG(bool, false));
 		}
+	}
 
-		obs_output_t *obs_frontend_get_virtualcam_output(void) override
-		{
-			OBSOutput output =
-				main->outputHandler->virtualCam.Get();
-			return obs_output_get_ref(output);
-		}
+	void obs_frontend_take_screenshot(void) override
+	{
+		QMetaObject::invokeMethod(main, "Screenshot");
+	}
 
-		void obs_frontend_start_virtualcam(void) override
-		{
-			QMetaObject::invokeMethod(main, "StartVirtualCam");
-		}
+	void obs_frontend_take_source_screenshot(obs_source_t *source) override
+	{
+		QMetaObject::invokeMethod(main, "Screenshot",
+					  Q_ARG(OBSSource, OBSSource(source)));
+	}
 
-		void obs_frontend_stop_virtualcam(void) override
-		{
-			QMetaObject::invokeMethod(main, "StopVirtualCam");
-		}
+	obs_output_t *obs_frontend_get_virtualcam_output(void) override
+	{
+		OBSOutput output = main->outputHandler->virtualCam.Get();
+		return obs_output_get_ref(output);
+	}
 
-		bool obs_frontend_virtualcam_active(void) override
-		{
-			return os_atomic_load_bool(&virtualcam_active);
-		}
+	void obs_frontend_start_virtualcam(void) override
+	{
+		QMetaObject::invokeMethod(main, "StartVirtualCam");
+	}
 
-		void obs_frontend_reset_video(void) override
-		{
-			main->ResetVideo();
-		}
+	void obs_frontend_stop_virtualcam(void) override
+	{
+		QMetaObject::invokeMethod(main, "StopVirtualCam");
+	}
 
-		void obs_frontend_open_source_properties(obs_source_t * source)
-			override
-		{
-			QMetaObject::invokeMethod(main, "OpenProperties",
-						  Q_ARG(OBSSource,
-							OBSSource(source)));
-		}
+	bool obs_frontend_virtualcam_active(void) override
+	{
+		return os_atomic_load_bool(&virtualcam_active);
+	}
 
-		void obs_frontend_open_source_filters(obs_source_t * source)
-			override
-		{
-			QMetaObject::invokeMethod(main, "OpenFilters",
-						  Q_ARG(OBSSource,
-							OBSSource(source)));
-		}
+	void obs_frontend_reset_video(void) override { main->ResetVideo(); }
 
-		void obs_frontend_open_source_interaction(obs_source_t * source)
-			override
-		{
-			QMetaObject::invokeMethod(main, "OpenInteraction",
-						  Q_ARG(OBSSource,
-							OBSSource(source)));
-		}
+	void obs_frontend_open_source_properties(obs_source_t *source) override
+	{
+		QMetaObject::invokeMethod(main, "OpenProperties",
+					  Q_ARG(OBSSource, OBSSource(source)));
+	}
 
-		void obs_frontend_open_sceneitem_edit_transform(
-			obs_sceneitem_t * item) override
-		{
-			QMetaObject::invokeMethod(main, "OpenEditTransform",
-						  Q_ARG(OBSSceneItem,
-							OBSSceneItem(item)));
-		}
+	void obs_frontend_open_source_filters(obs_source_t *source) override
+	{
+		QMetaObject::invokeMethod(main, "OpenFilters",
+					  Q_ARG(OBSSource, OBSSource(source)));
+	}
 
-		char *obs_frontend_get_current_record_output_path(void) override
-		{
-			const char *recordOutputPath =
-				main->GetCurrentOutputPath();
+	void obs_frontend_open_source_interaction(obs_source_t *source) override
+	{
+		QMetaObject::invokeMethod(main, "OpenInteraction",
+					  Q_ARG(OBSSource, OBSSource(source)));
+	}
 
-			return bstrdup(recordOutputPath);
-		}
+	void obs_frontend_open_sceneitem_edit_transform(
+		obs_sceneitem_t *item) override
+	{
+		QMetaObject::invokeMethod(main, "OpenEditTransform",
+					  Q_ARG(OBSSceneItem,
+						OBSSceneItem(item)));
+	}
 
-		const char *obs_frontend_get_locale_string(const char *string)
-			override
-		{
-			return Str(string);
-		}
+	char *obs_frontend_get_current_record_output_path(void) override
+	{
+		const char *recordOutputPath = main->GetCurrentOutputPath();
 
-		bool obs_frontend_is_theme_dark(void) override
-		{
-			return App()->IsThemeDark();
-		}
+		return bstrdup(recordOutputPath);
+	}
 
-		char *obs_frontend_get_last_recording(void) override
-		{
-			return bstrdup(
-				main->outputHandler->lastRecordingPath.c_str());
-		}
+	const char *obs_frontend_get_locale_string(const char *string) override
+	{
+		return Str(string);
+	}
 
-		char *obs_frontend_get_last_screenshot(void) override
-		{
-			return bstrdup(main->lastScreenshot.c_str());
-		}
+	bool obs_frontend_is_theme_dark(void) override
+	{
+		return App()->IsThemeDark();
+	}
 
-		char *obs_frontend_get_last_replay(void) override
-		{
-			return bstrdup(main->lastReplay.c_str());
-		}
+	char *obs_frontend_get_last_recording(void) override
+	{
+		return bstrdup(main->outputHandler->lastRecordingPath.c_str());
+	}
 
-		void obs_frontend_add_undo_redo_action(
-			const char *name, const undo_redo_cb undo,
-			const undo_redo_cb redo, const char *undo_data,
-			const char *redo_data, bool repeatable) override
-		{
-			main->undo_s.add_action(
-				name,
-				[undo](const std::string &data) {
-					undo(data.c_str());
-				},
-				[redo](const std::string &data) {
-					redo(data.c_str());
-				},
-				undo_data, redo_data, repeatable);
-		}
+	char *obs_frontend_get_last_screenshot(void) override
+	{
+		return bstrdup(main->lastScreenshot.c_str());
+	}
 
-		void on_load(obs_data_t * settings) override
-		{
-			for (size_t i = saveCallbacks.size(); i > 0; i--) {
-				auto cb = saveCallbacks[i - 1];
-				cb.callback(settings, false, cb.private_data);
-			}
-		}
+	char *obs_frontend_get_last_replay(void) override
+	{
+		return bstrdup(main->lastReplay.c_str());
+	}
 
-		void on_preload(obs_data_t * settings) override
-		{
-			for (size_t i = preloadCallbacks.size(); i > 0; i--) {
-				auto cb = preloadCallbacks[i - 1];
-				cb.callback(settings, false, cb.private_data);
-			}
+	void obs_frontend_add_undo_redo_action(const char *name,
+					       const undo_redo_cb undo,
+					       const undo_redo_cb redo,
+					       const char *undo_data,
+					       const char *redo_data,
+					       bool repeatable) override
+	{
+		main->undo_s.add_action(
+			name,
+			[undo](const std::string &data) { undo(data.c_str()); },
+			[redo](const std::string &data) { redo(data.c_str()); },
+			undo_data, redo_data, repeatable);
+	}
+
+	void on_load(obs_data_t *settings) override
+	{
+		for (size_t i = saveCallbacks.size(); i > 0; i--) {
+			auto cb = saveCallbacks[i - 1];
+			cb.callback(settings, false, cb.private_data);
 		}
+	}
 
-		void on_save(obs_data_t * settings) override
-		{
-			for (size_t i = saveCallbacks.size(); i > 0; i--) {
-				auto cb = saveCallbacks[i - 1];
-				cb.callback(settings, true, cb.private_data);
-			}
+	void on_preload(obs_data_t *settings) override
+	{
+		for (size_t i = preloadCallbacks.size(); i > 0; i--) {
+			auto cb = preloadCallbacks[i - 1];
+			cb.callback(settings, false, cb.private_data);
 		}
+	}
 
-		void on_event(enum obs_frontend_event event) override
-		{
-			if (main->disableSaving &&
-			    event !=
-				    OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP &&
-			    event != OBS_FRONTEND_EVENT_EXIT)
-				return;
-
-			for (size_t i = callbacks.size(); i > 0; i--) {
-				auto cb = callbacks[i - 1];
-				cb.callback(event, cb.private_data);
-			}
+	void on_save(obs_data_t *settings) override
+	{
+		for (size_t i = saveCallbacks.size(); i > 0; i--) {
+			auto cb = saveCallbacks[i - 1];
+			cb.callback(settings, true, cb.private_data);
 		}
-	};
+	}
 
-	obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main)
+	void on_event(enum obs_frontend_event event) override
 	{
-		obs_frontend_callbacks *api = new OBSStudioAPI(main);
-		obs_frontend_set_callbacks_internal(api);
-		return api;
+		if (main->disableSaving &&
+		    event != OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP &&
+		    event != OBS_FRONTEND_EVENT_EXIT)
+			return;
+
+		for (size_t i = callbacks.size(); i > 0; i--) {
+			auto cb = callbacks[i - 1];
+			cb.callback(event, cb.private_data);
+		}
 	}
+};
+
+obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main)
+{
+	obs_frontend_callbacks *api = new OBSStudioAPI(main);
+	obs_frontend_set_callbacks_internal(api);
+	return api;
+}

+ 81 - 128
UI/obs-app.cpp

@@ -609,109 +609,41 @@ static bool MakeUserDirs()
 	return true;
 }
 
-static bool MakeUserProfileDirs()
-{
-	char path[512];
-
-	if (GetConfigPath(path, sizeof(path), "obs-studio/basic/profiles") <= 0)
-		return false;
-	if (!do_mkdir(path))
-		return false;
+constexpr std::string_view OBSProfileSubDirectory = "obs-studio/basic/profiles";
+constexpr std::string_view OBSScenesSubDirectory = "obs-studio/basic/scenes";
 
-	if (GetConfigPath(path, sizeof(path), "obs-studio/basic/scenes") <= 0)
-		return false;
-	if (!do_mkdir(path))
-		return false;
-
-	return true;
-}
-
-static string GetProfileDirFromName(const char *name)
+static bool MakeUserProfileDirs()
 {
-	string outputPath;
-	os_glob_t *glob;
-	char path[512];
-
-	if (GetConfigPath(path, sizeof(path), "obs-studio/basic/profiles") <= 0)
-		return outputPath;
-
-	strcat(path, "/*");
-
-	if (os_glob(path, 0, &glob) != 0)
-		return outputPath;
-
-	for (size_t i = 0; i < glob->gl_pathc; i++) {
-		struct os_globent ent = glob->gl_pathv[i];
-		if (!ent.directory)
-			continue;
-
-		strcpy(path, ent.path);
-		strcat(path, "/basic.ini");
-
-		ConfigFile config;
-		if (config.Open(path, CONFIG_OPEN_EXISTING) != 0)
-			continue;
-
-		const char *curName =
-			config_get_string(config, "General", "Name");
-		if (astrcmpi(curName, name) == 0) {
-			outputPath = ent.path;
-			break;
+	const std::filesystem::path userProfilePath =
+		App()->userProfilesLocation /
+		std::filesystem::u8path(OBSProfileSubDirectory);
+	const std::filesystem::path userScenesPath =
+		App()->userScenesLocation /
+		std::filesystem::u8path(OBSScenesSubDirectory);
+
+	if (!std::filesystem::exists(userProfilePath)) {
+		try {
+			std::filesystem::create_directories(userProfilePath);
+		} catch (const std::filesystem::filesystem_error &error) {
+			blog(LOG_ERROR,
+			     "Failed to create user profile directory '%s'\n%s",
+			     userProfilePath.u8string().c_str(), error.what());
+			return false;
 		}
 	}
 
-	os_globfree(glob);
-
-	if (!outputPath.empty()) {
-		replace(outputPath.begin(), outputPath.end(), '\\', '/');
-		const char *start = strrchr(outputPath.c_str(), '/');
-		if (start)
-			outputPath.erase(0, start - outputPath.c_str() + 1);
-	}
-
-	return outputPath;
-}
-
-static string GetSceneCollectionFileFromName(const char *name)
-{
-	string outputPath;
-	os_glob_t *glob;
-	char path[512];
-
-	if (GetConfigPath(path, sizeof(path), "obs-studio/basic/scenes") <= 0)
-		return outputPath;
-
-	strcat(path, "/*.json");
-
-	if (os_glob(path, 0, &glob) != 0)
-		return outputPath;
-
-	for (size_t i = 0; i < glob->gl_pathc; i++) {
-		struct os_globent ent = glob->gl_pathv[i];
-		if (ent.directory)
-			continue;
-
-		OBSDataAutoRelease data =
-			obs_data_create_from_json_file_safe(ent.path, "bak");
-		const char *curName = obs_data_get_string(data, "name");
-
-		if (astrcmpi(name, curName) == 0) {
-			outputPath = ent.path;
-			break;
+	if (!std::filesystem::exists(userScenesPath)) {
+		try {
+			std::filesystem::create_directories(userScenesPath);
+		} catch (const std::filesystem::filesystem_error &error) {
+			blog(LOG_ERROR,
+			     "Failed to create user scene collection directory '%s'\n%s",
+			     userScenesPath.u8string().c_str(), error.what());
+			return false;
 		}
 	}
 
-	os_globfree(glob);
-
-	if (!outputPath.empty()) {
-		outputPath.resize(outputPath.size() - 5);
-		replace(outputPath.begin(), outputPath.end(), '\\', '/');
-		const char *start = strrchr(outputPath.c_str(), '/');
-		if (start)
-			outputPath.erase(0, start - outputPath.c_str() + 1);
-	}
-
-	return outputPath;
+	return true;
 }
 
 bool OBSApp::UpdatePre22MultiviewLayout(const char *layout)
@@ -1212,56 +1144,77 @@ OBSApp::~OBSApp()
 static void move_basic_to_profiles(void)
 {
 	char path[512];
-	char new_path[512];
-	os_glob_t *glob;
 
-	/* if not first time use */
-	if (GetConfigPath(path, 512, "obs-studio/basic") <= 0)
-		return;
-	if (!os_file_exists(path))
+	if (GetAppConfigPath(path, 512, "obs-studio/basic") <= 0) {
 		return;
+	}
 
-	/* if the profiles directory doesn't already exist */
-	if (GetConfigPath(new_path, 512, "obs-studio/basic/profiles") <= 0)
-		return;
-	if (os_file_exists(new_path))
-		return;
+	const std::filesystem::path basicPath = std::filesystem::u8path(path);
 
-	if (os_mkdir(new_path) == MKDIR_ERROR)
+	if (!std::filesystem::exists(basicPath)) {
 		return;
+	}
 
-	strcat(new_path, "/");
-	strcat(new_path, Str("Untitled"));
-	if (os_mkdir(new_path) == MKDIR_ERROR)
-		return;
+	const std::filesystem::path profilesPath =
+		App()->userProfilesLocation /
+		std::filesystem::u8path("obs-studio/basic/profiles");
 
-	strcat(path, "/*.*");
-	if (os_glob(path, 0, &glob) != 0)
+	if (std::filesystem::exists(profilesPath)) {
 		return;
+	}
 
-	strcpy(path, new_path);
+	try {
+		std::filesystem::create_directories(profilesPath);
+	} catch (const std::filesystem::filesystem_error &error) {
+		blog(LOG_ERROR,
+		     "Failed to create profiles directory for migration from basic profile\n%s",
+		     error.what());
+		return;
+	}
 
-	for (size_t i = 0; i < glob->gl_pathc; i++) {
-		struct os_globent ent = glob->gl_pathv[i];
-		char *file;
+	const std::filesystem::path newProfilePath =
+		profilesPath / std::filesystem::u8path(Str("Untitled"));
 
-		if (ent.directory)
+	for (auto &entry : std::filesystem::directory_iterator(basicPath)) {
+		if (entry.is_directory()) {
 			continue;
+		}
 
-		file = strrchr(ent.path, '/');
-		if (!file++)
+		if (entry.path().filename().u8string() == "scenes.json") {
 			continue;
+		}
 
-		if (astrcmpi(file, "scenes.json") == 0)
-			continue;
+		if (!std::filesystem::exists(newProfilePath)) {
+			try {
+				std::filesystem::create_directory(
+					newProfilePath);
+			} catch (
+				const std::filesystem::filesystem_error &error) {
+				blog(LOG_ERROR,
+				     "Failed to create profile directory for 'Untitled'\n%s",
+				     error.what());
+				return;
+			}
+		}
 
-		strcpy(new_path, path);
-		strcat(new_path, "/");
-		strcat(new_path, file);
-		os_rename(ent.path, new_path);
-	}
+		const filesystem::path destinationFile =
+			newProfilePath / entry.path().filename();
 
-	os_globfree(glob);
+		const auto copyOptions =
+			std::filesystem::copy_options::overwrite_existing;
+
+		try {
+			std::filesystem::copy(entry.path(), destinationFile,
+					      copyOptions);
+		} catch (const std::filesystem::filesystem_error &error) {
+			blog(LOG_ERROR,
+			     "Failed to copy basic profile file '%s' to new profile 'Untitled'\n%s",
+			     entry.path().filename().u8string().c_str(),
+			     error.what());
+
+			return;
+		}
+	}
 }
 
 static void move_basic_to_scene_collections(void)

+ 13 - 7
UI/window-basic-auto-config.cpp

@@ -39,18 +39,24 @@ extern QCefCookieManager *panel_cookies;
 
 /* ------------------------------------------------------------------------- */
 
-#define SERVICE_PATH "service.json"
+constexpr std::string_view OBSServiceFileName = "service.json";
 
 static OBSData OpenServiceSettings(std::string &type)
 {
-	char serviceJsonPath[512];
-	int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath),
-				 SERVICE_PATH);
-	if (ret <= 0)
+	const OBSBasic *basic =
+		reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+	const OBSProfile &currentProfile = basic->GetCurrentProfile();
+
+	const std::filesystem::path jsonFilePath =
+		currentProfile.path /
+		std::filesystem::u8path(OBSServiceFileName);
+
+	if (!std::filesystem::exists(jsonFilePath)) {
 		return OBSData();
+	}
 
-	OBSDataAutoRelease data =
-		obs_data_create_from_json_file_safe(serviceJsonPath, "bak");
+	OBSDataAutoRelease data = obs_data_create_from_json_file_safe(
+		jsonFilePath.u8string().c_str(), "bak");
 
 	obs_data_set_default_string(data, "type", "rtmp_common");
 	type = obs_data_get_string(data, "type");

+ 14 - 5
UI/window-basic-main-outputs.cpp

@@ -1616,19 +1616,28 @@ struct AdvancedOutput : BasicOutputHandler {
 
 static OBSData GetDataFromJsonFile(const char *jsonFile)
 {
-	char fullPath[512];
+	const OBSBasic *basic =
+		reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+
+	const OBSProfile &currentProfile = basic->GetCurrentProfile();
+
+	const std::filesystem::path jsonFilePath =
+		currentProfile.path / std::filesystem::u8path(jsonFile);
+
 	OBSDataAutoRelease data = nullptr;
 
-	int ret = GetProfilePath(fullPath, sizeof(fullPath), jsonFile);
-	if (ret > 0) {
-		BPtr<char> jsonData = os_quick_read_utf8_file(fullPath);
+	if (!jsonFilePath.empty()) {
+		BPtr<char> jsonData = os_quick_read_utf8_file(
+			jsonFilePath.u8string().c_str());
+
 		if (!!jsonData) {
 			data = obs_data_create_from_json(jsonData);
 		}
 	}
 
-	if (!data)
+	if (!data) {
 		data = obs_data_create();
+	}
 
 	return data.Get();
 }

文件差異過大導致無法顯示
+ 552 - 572
UI/window-basic-main-profiles.cpp


文件差異過大導致無法顯示
+ 354 - 280
UI/window-basic-main.cpp


+ 84 - 23
UI/window-basic-main.hpp

@@ -134,6 +134,37 @@ private:
 	std::shared_ptr<OBSSignal> renamedSignal;
 };
 
+struct OBSProfile {
+	std::string name;
+	std::string directoryName;
+	std::filesystem::path path;
+	std::filesystem::path profileFile;
+};
+
+struct OBSSceneCollection {
+	std::string name;
+	std::string fileName;
+	std::filesystem::path collectionFile;
+};
+
+struct OBSPromptResult {
+	bool success;
+	std::string promptValue;
+	bool optionValue;
+};
+
+struct OBSPromptRequest {
+	std::string title;
+	std::string prompt;
+	std::string promptValue;
+	bool withOption;
+	std::string optionPrompt;
+	bool optionValue;
+};
+
+using OBSPromptCallback = std::function<bool(const OBSPromptResult &result)>;
+
+using OBSProfileCache = std::map<std::string, OBSProfile>;
 class ColorSelect : public QWidget {
 
 public:
@@ -443,17 +474,6 @@ private:
 	void RefreshSceneCollections();
 	void ChangeSceneCollection();
 	void LogScenes();
-
-	void ResetProfileData();
-	bool AddProfile(bool create_new, const char *title, const char *text,
-			const char *init_text = nullptr, bool rename = false);
-	bool CreateProfile(const std::string &newName, bool create_new,
-			   bool showWizardChecked, bool rename = false);
-	void DeleteProfile(const char *profile_name, const char *profile_dir);
-	void RefreshProfiles();
-	void ChangeProfile();
-	void CheckForSimpleModeX264Fallback();
-
 	void SaveProjectNow();
 
 	int GetTopSelectedSourceItem();
@@ -742,11 +762,6 @@ public slots:
 
 	bool AddSceneCollection(bool create_new,
 				const QString &name = QString());
-
-	bool NewProfile(const QString &name);
-	bool DuplicateProfile(const QString &name);
-	void DeleteProfile(const QString &profileName);
-
 	void UpdatePatronJson(const QString &text, const QString &error);
 
 	void ShowContextBar();
@@ -1156,13 +1171,6 @@ private slots:
 	void on_actionExportSceneCollection_triggered();
 	void on_actionRemigrateSceneCollection_triggered();
 
-	void on_actionNewProfile_triggered();
-	void on_actionDupProfile_triggered();
-	void on_actionRenameProfile_triggered();
-	void on_actionRemoveProfile_triggered(bool skipConfirmation = false);
-	void on_actionImportProfile_triggered();
-	void on_actionExportProfile_triggered();
-
 	void on_actionShowSettingsFolder_triggered();
 	void on_actionShowProfileFolder_triggered();
 
@@ -1337,6 +1345,59 @@ public:
 	void DeleteYouTubeAppDock();
 	YouTubeAppDock *GetYouTubeAppDock();
 #endif
+	// MARK: - Generic UI Helper Functions
+	OBSPromptResult PromptForName(const OBSPromptRequest &request,
+				      const OBSPromptCallback &callback);
+
+	// MARK: - OBS Profile Management
+private:
+	OBSProfileCache profiles{};
+
+	void SetupNewProfile(const std::string &profileName,
+			     bool useWizard = false);
+	void SetupDuplicateProfile(const std::string &profileName);
+	void SetupRenameProfile(const std::string &profileName);
+
+	const OBSProfile &CreateProfile(const std::string &profileName);
+	void RemoveProfile(OBSProfile profile);
+
+	void ChangeProfile();
+
+	void RefreshProfileCache();
+
+	void RefreshProfiles(bool refreshCache = false);
+
+	void ActivateProfile(const OBSProfile &profile, bool reset = false);
+	std::vector<std::string>
+	GetRestartRequirements(const ConfigFile &config) const;
+	void ResetProfileData();
+	void CheckForSimpleModeX264Fallback();
+
+public:
+	inline const OBSProfileCache &GetProfileCache() const noexcept
+	{
+		return profiles;
+	};
+
+	const OBSProfile &GetCurrentProfile() const;
+
+	std::optional<OBSProfile>
+	GetProfileByName(const std::string &profileName) const;
+	std::optional<OBSProfile>
+	GetProfileByDirectoryName(const std::string &directoryName) const;
+
+private slots:
+	void on_actionNewProfile_triggered();
+	void on_actionDupProfile_triggered();
+	void on_actionRenameProfile_triggered();
+	void on_actionRemoveProfile_triggered(bool skipConfirmation = false);
+	void on_actionImportProfile_triggered();
+	void on_actionExportProfile_triggered();
+
+public slots:
+	bool CreateNewProfile(const QString &name);
+	bool CreateDuplicateProfile(const QString &name);
+	void DeleteProfile(const QString &profileName);
 };
 
 extern bool cef_js_avail;

+ 28 - 17
UI/window-basic-settings.cpp

@@ -2136,12 +2136,16 @@ OBSBasicSettings::CreateEncoderPropertyView(const char *encoder,
 	OBSPropertiesView *view;
 
 	if (path) {
-		char encoderJsonPath[512];
-		int ret = GetProfilePath(encoderJsonPath,
-					 sizeof(encoderJsonPath), path);
-		if (ret > 0) {
+		const OBSBasic *basic =
+			reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+		const OBSProfile &currentProfile = basic->GetCurrentProfile();
+
+		const std::filesystem::path jsonFilePath =
+			currentProfile.path / std::filesystem::u8path(path);
+
+		if (!jsonFilePath.empty()) {
 			obs_data_t *data = obs_data_create_from_json_file_safe(
-				encoderJsonPath, "bak");
+				jsonFilePath.u8string().c_str(), "bak");
 			obs_data_apply(settings, data);
 			obs_data_release(data);
 		}
@@ -3748,17 +3752,22 @@ static inline const char *SplitFileTypeFromIdx(int idx)
 
 static void WriteJsonData(OBSPropertiesView *view, const char *path)
 {
-	char full_path[512];
-
 	if (!view || !WidgetChanged(view))
 		return;
 
-	int ret = GetProfilePath(full_path, sizeof(full_path), path);
-	if (ret > 0) {
+	const OBSBasic *basic =
+		reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+	const OBSProfile &currentProfile = basic->GetCurrentProfile();
+
+	const std::filesystem::path jsonFilePath =
+		currentProfile.path / std::filesystem::u8path(path);
+
+	if (!jsonFilePath.empty()) {
 		obs_data_t *settings = view->GetSettings();
 		if (settings) {
-			obs_data_save_json_safe(settings, full_path, "tmp",
-						"bak");
+			obs_data_save_json_safe(settings,
+						jsonFilePath.u8string().c_str(),
+						"tmp", "bak");
 		}
 	}
 }
@@ -5691,14 +5700,16 @@ void OBSBasicSettings::AdvReplayBufferChanged()
 		if (!settings)
 			return;
 
-		char encoderJsonPath[512];
-		int ret = GetProfilePath(encoderJsonPath,
-					 sizeof(encoderJsonPath),
-					 "recordEncoder.json");
-		if (ret > 0) {
+		const OBSProfile &currentProfile = main->GetCurrentProfile();
+
+		const std::filesystem::path jsonFilePath =
+			currentProfile.path /
+			std::filesystem::u8path("recordEncoder.json");
+
+		if (!jsonFilePath.empty()) {
 			OBSDataAutoRelease data =
 				obs_data_create_from_json_file_safe(
-					encoderJsonPath, "bak");
+					jsonFilePath.u8string().c_str(), "bak");
 			obs_data_apply(settings, data);
 		}
 	}

+ 40 - 38
UI/window-youtube-actions.cpp

@@ -287,8 +287,8 @@ OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth,
 	workerThread->start();
 
 	OBSBasic *main = OBSBasic::Get();
-	bool rememberSettings = config_get_bool(main->basicConfig, "YouTube",
-						"RememberSettings");
+	bool rememberSettings = config_get_bool(main->activeConfiguration,
+						"YouTube", "RememberSettings");
 	if (rememberSettings)
 		LoadSettings();
 
@@ -749,83 +749,85 @@ void OBSYoutubeActions::SaveSettings(BroadcastDescription &broadcast)
 {
 	OBSBasic *main = OBSBasic::Get();
 
-	config_set_string(main->basicConfig, "YouTube", "Title",
+	config_set_string(main->activeConfiguration, "YouTube", "Title",
 			  QT_TO_UTF8(broadcast.title));
-	config_set_string(main->basicConfig, "YouTube", "Description",
+	config_set_string(main->activeConfiguration, "YouTube", "Description",
 			  QT_TO_UTF8(broadcast.description));
-	config_set_string(main->basicConfig, "YouTube", "Privacy",
+	config_set_string(main->activeConfiguration, "YouTube", "Privacy",
 			  QT_TO_UTF8(broadcast.privacy));
-	config_set_string(main->basicConfig, "YouTube", "CategoryID",
+	config_set_string(main->activeConfiguration, "YouTube", "CategoryID",
 			  QT_TO_UTF8(broadcast.category.id));
-	config_set_string(main->basicConfig, "YouTube", "Latency",
+	config_set_string(main->activeConfiguration, "YouTube", "Latency",
 			  QT_TO_UTF8(broadcast.latency));
-	config_set_bool(main->basicConfig, "YouTube", "MadeForKids",
+	config_set_bool(main->activeConfiguration, "YouTube", "MadeForKids",
 			broadcast.made_for_kids);
-	config_set_bool(main->basicConfig, "YouTube", "AutoStart",
+	config_set_bool(main->activeConfiguration, "YouTube", "AutoStart",
 			broadcast.auto_start);
-	config_set_bool(main->basicConfig, "YouTube", "AutoStop",
+	config_set_bool(main->activeConfiguration, "YouTube", "AutoStop",
 			broadcast.auto_start);
-	config_set_bool(main->basicConfig, "YouTube", "DVR", broadcast.dvr);
-	config_set_bool(main->basicConfig, "YouTube", "ScheduleForLater",
-			broadcast.schedul_for_later);
-	config_set_string(main->basicConfig, "YouTube", "Projection",
+	config_set_bool(main->activeConfiguration, "YouTube", "DVR",
+			broadcast.dvr);
+	config_set_bool(main->activeConfiguration, "YouTube",
+			"ScheduleForLater", broadcast.schedul_for_later);
+	config_set_string(main->activeConfiguration, "YouTube", "Projection",
 			  QT_TO_UTF8(broadcast.projection));
-	config_set_string(main->basicConfig, "YouTube", "ThumbnailFile",
+	config_set_string(main->activeConfiguration, "YouTube", "ThumbnailFile",
 			  QT_TO_UTF8(thumbnailFile));
-	config_set_bool(main->basicConfig, "YouTube", "RememberSettings", true);
+	config_set_bool(main->activeConfiguration, "YouTube",
+			"RememberSettings", true);
 }
 
 void OBSYoutubeActions::LoadSettings()
 {
 	OBSBasic *main = OBSBasic::Get();
 
-	const char *title =
-		config_get_string(main->basicConfig, "YouTube", "Title");
+	const char *title = config_get_string(main->activeConfiguration,
+					      "YouTube", "Title");
 	ui->title->setText(QT_UTF8(title));
 
-	const char *desc =
-		config_get_string(main->basicConfig, "YouTube", "Description");
+	const char *desc = config_get_string(main->activeConfiguration,
+					     "YouTube", "Description");
 	ui->description->setPlainText(QT_UTF8(desc));
 
-	const char *priv =
-		config_get_string(main->basicConfig, "YouTube", "Privacy");
+	const char *priv = config_get_string(main->activeConfiguration,
+					     "YouTube", "Privacy");
 	int index = ui->privacyBox->findData(priv);
 	ui->privacyBox->setCurrentIndex(index);
 
-	const char *catID =
-		config_get_string(main->basicConfig, "YouTube", "CategoryID");
+	const char *catID = config_get_string(main->activeConfiguration,
+					      "YouTube", "CategoryID");
 	index = ui->categoryBox->findData(catID);
 	ui->categoryBox->setCurrentIndex(index);
 
-	const char *latency =
-		config_get_string(main->basicConfig, "YouTube", "Latency");
+	const char *latency = config_get_string(main->activeConfiguration,
+						"YouTube", "Latency");
 	index = ui->latencyBox->findData(latency);
 	ui->latencyBox->setCurrentIndex(index);
 
-	bool dvr = config_get_bool(main->basicConfig, "YouTube", "DVR");
+	bool dvr = config_get_bool(main->activeConfiguration, "YouTube", "DVR");
 	ui->checkDVR->setChecked(dvr);
 
-	bool forKids =
-		config_get_bool(main->basicConfig, "YouTube", "MadeForKids");
+	bool forKids = config_get_bool(main->activeConfiguration, "YouTube",
+				       "MadeForKids");
 	if (forKids)
 		ui->yesMakeForKids->setChecked(true);
 	else
 		ui->notMakeForKids->setChecked(true);
 
-	bool schedLater = config_get_bool(main->basicConfig, "YouTube",
+	bool schedLater = config_get_bool(main->activeConfiguration, "YouTube",
 					  "ScheduleForLater");
 	ui->checkScheduledLater->setChecked(schedLater);
 
-	bool autoStart =
-		config_get_bool(main->basicConfig, "YouTube", "AutoStart");
+	bool autoStart = config_get_bool(main->activeConfiguration, "YouTube",
+					 "AutoStart");
 	ui->checkAutoStart->setChecked(autoStart);
 
-	bool autoStop =
-		config_get_bool(main->basicConfig, "YouTube", "AutoStop");
+	bool autoStop = config_get_bool(main->activeConfiguration, "YouTube",
+					"AutoStop");
 	ui->checkAutoStop->setChecked(autoStop);
 
-	const char *projection =
-		config_get_string(main->basicConfig, "YouTube", "Projection");
+	const char *projection = config_get_string(main->activeConfiguration,
+						   "YouTube", "Projection");
 	if (projection && *projection) {
 		if (strcmp(projection, "360") == 0)
 			ui->check360Video->setChecked(true);
@@ -833,8 +835,8 @@ void OBSYoutubeActions::LoadSettings()
 			ui->check360Video->setChecked(false);
 	}
 
-	const char *thumbFile = config_get_string(main->basicConfig, "YouTube",
-						  "ThumbnailFile");
+	const char *thumbFile = config_get_string(main->activeConfiguration,
+						  "YouTube", "ThumbnailFile");
 	if (thumbFile && *thumbFile) {
 		QFileInfo tFile(thumbFile);
 		// Re-check validity before setting path again

部分文件因文件數量過多而無法顯示