Editor/sandmod.libraryplus/Widget/LibraryManagerDock.cs
using System;
using System.IO;
using System.Linq;
using System.Threading;
using Editor;
using Sandbox;
using Sandbox.UI;
using FileSystem = Editor.FileSystem;

namespace LibraryPlus;

[Dock( "Editor", "Library Plus Manager", "extension" )]
public class LibraryManagerDock : Widget
{
	internal Action<string> OnValueChanged;
	private NavigationView View;

	public LibraryManagerDock( Widget parent ) : base( parent )
	{
		Layout = Layout.Row();
		Layout.Margin = 4;
		Layout.Spacing = 4;

		FocusMode = FocusMode.TabOrClickOrWheel;
	}

	[EditorEvent.Hotload]
	private void Rebuild()
	{
		Layout.Clear( true );

		View = new NavigationView( this );
		View.AddSectionHeader( "Project" );
		View.AddPage( "Installed", "folder",
			new InstalledLibrariesWidget( this ) { ContentMargins = new Margin( 8, 0, 0, 0 ) } );

		View.AddSectionHeader( "Install" );
		View.AddPage( "sbox.game", "language",
			new AvailableLibrariesWidget( this ) { ContentMargins = new Margin( 8, 0, 0, 0 ) } );


		{
			var row = View.MenuTop.AddRow();
			row.Spacing = 8;
			var textEdit = row.Add( new LineEdit( this )
			{
				FixedHeight = Theme.RowHeight, PlaceholderText = "⌕  Search", ToolTip = "Search"
			} );
			textEdit.TextChanged += txt =>
			{
				OnValueChanged?.Invoke( txt );
			};
			textEdit.SetStyles( "background-color: #000;" );
			row.Add( new IconButton( "folder" )
			{
				ToolTip = "Open Library Folder",
				OnClick = () => EditorUtility.OpenFolder( FileSystem.Libraries.GetFullPath( "/" ) )
			} );
			row.Add( new IconButton( "add_circle" ) { ToolTip = "Create New Library", OnClick = CreateNewLibrary } );
		}

		Layout.Add( View );
	}

	private void CreateNewLibrary()
	{
		Dialog.AskString( CreateNewLibrary, "What would you like to call your new library?", "Create",
			title: "Create a Library" );
	}

	private void CreateNewLibrary( string libraryName )
	{
		// make libraryName folder safe
		var folderName = string.Concat( libraryName.Where( char.IsAsciiLetterOrDigit ) );
		if ( string.IsNullOrWhiteSpace( folderName ) )
		{
			return;
		}

		void FinalizeLibrary( string name )
		{
			FileSystem.Libraries.CreateDirectory( name );
			var dir = FileSystem.Libraries.GetFullPath( name );

			CopyFolder( FileSystem.Root.GetFullPath( "templates/library.minimal" ), dir, libraryName, name.ToLower() );

			_ = LibrarySystem.Add( name, CancellationToken.None );
		}

		// Check to make sure the ident doesn't match the game's ident or any other library
		var sharesIdent = string.Equals( folderName, Project.Current.Package.Ident,
			StringComparison.CurrentCultureIgnoreCase );
		if ( !sharesIdent )
		{
			sharesIdent = FileSystem.Libraries.FindDirectory( "", folderName )?.Count() > 0;
		}

		if ( sharesIdent )
		{
			var newIdentNum = 2;
			var newFolderName = $"{folderName}_{newIdentNum}";
			while ( (FileSystem.Libraries.FindDirectory( "", newFolderName )?.Count() ?? 0) > 0 )
			{
				newIdentNum++;
				newFolderName = $"{folderName}_{newIdentNum}";
			}

			folderName = newFolderName;
			Dialog.AskConfirm( () => { FinalizeLibrary( newFolderName ); },
				$"The library cannot share it's Ident with another Package. Would you like to continue with the ident \"{newFolderName}\"?",
				"Error creating Library", "OK" );
			return;
		}

		FinalizeLibrary( folderName );
	}

	private static void CopyFolder( string sourceDir, string targetDir, string libraryName, string libraryFolder )
	{
		if ( sourceDir.Contains( "\\.", StringComparison.OrdinalIgnoreCase ) )
		{
			return;
		}

		Directory.CreateDirectory( targetDir );

		foreach ( var file in Directory.GetFiles( sourceDir ) )
		{
			CopyAndProcessFile( file, targetDir, libraryName, libraryFolder );
		}

		foreach ( var directory in Directory.GetDirectories( sourceDir ) )
		{
			CopyFolder( directory, Path.Combine( targetDir, Path.GetFileName( directory ) ), libraryName,
				libraryFolder );
		}
	}

	private static void CopyAndProcessFile( string file, string targetDir, string libraryName, string libraryFolder )
	{
		var targetname = Path.Combine( targetDir, Path.GetFileName( file ) );

		// Replace $ident with our ident in file name
		targetname = targetname.Replace( "$title", libraryName );
		targetname = targetname.Replace( "$ident", libraryFolder );

		if ( file.EndsWith( ".cs" ) || file.EndsWith( ".json" ) || file.EndsWith( ".sbproj" ) )
		{
			var txt = File.ReadAllText( file );
			txt = txt.Replace( "$title", libraryName );
			txt = txt.Replace( "$ident", libraryFolder );
			File.WriteAllText( targetname, txt );
		}
		else
		{
			File.Copy( file, targetname );
		}
	}

	[EditorEvent.Frame]
	public void OnFrame()
	{
		if ( SetContentHash( ContentHash, 0.5f ) )
		{
			Rebuild();
		}
	}

	private int ContentHash()
	{
		return HashCode.Combine( 0 );
	}
}

public class InstalledLibrariesWidget : Widget
{
	private readonly Layout ContentLayout;

	public InstalledLibrariesWidget( LibraryManagerDock manager )
	{
		Layout = Layout.Row();

		var left = new LibraryList( null ) { ShowInstalled = true };

		Layout.Add( left, 1 );
		ContentLayout = Layout.AddColumn();

		left.OnLibrarySelected = library =>
		{
			ContentLayout.Clear( true );
			ContentLayout.Add( new LibraryDetail( library ) );
		};

		manager.OnValueChanged = txt =>
		{
			left.Filter = txt;
		};
	}
}

public class AvailableLibrariesWidget : Widget
{
	private readonly Layout ContentLayout;

	public AvailableLibrariesWidget( LibraryManagerDock manager )
	{
		Layout = Layout.Row();

		var left = new LibraryList( null ) { ShowAvailable = true };

		Layout.Add( left, 1 );
		ContentLayout = Layout.AddColumn();

		left.OnLibrarySelected = library =>
		{
			ContentLayout.Clear( true );
			ContentLayout.Add( new LibraryDetail( library ) );
		};

		manager.OnValueChanged = txt =>
		{
			left.Filter = txt;
		};
	}
}