Put everything in a monodevelop project.

This commit is contained in:
Hylke Bons 2010-05-03 01:04:39 +01:00
parent 90a067a2a3
commit ef235a14e6
24 changed files with 1388 additions and 1079 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
*~
*.exe
*.exe.mdb

View file

@ -1,26 +1,27 @@
SparklePony.exe : src/SparklePony.cs
gmcs -pkg:gtk-sharp-2.0 -pkg:notify-sharp -pkg:dbus-sharp src/SparklePony.cs
SparkleShare.exe : SparkleShare.sln
mdtool build --f --buildfile:SparkleShare.sln
install:
mkdir -p /usr/local/share/sparklepony
cp src/SparklePony.exe /usr/local/share/sparklepony/
chmod 755 /usr/local/share/sparklepony/SparklePony.exe
cp src/sparklepony /usr/local/bin/
chmod 755 /usr/local/bin/sparklepony
mkdir -p /usr/local/share/sparkleshare
cp SparkleShare/bin/Debug/SparkleShare.exe /usr/local/share/sparkleshare/
cp SparkleShare/bin/Debug/SparkleShare.exe.mdb /usr/local/share/sparkleshare/
chmod 755 /usr/local/share/sparkleshare/SparkleShare.exe
cp sparkleshare /usr/local/bin/
chmod 755 /usr/local/bin/sparkleshare
cp data/icons /usr/share/ -R
mkdir -p ~/.config/autostart
cp data/sparklepony.desktop.in ~/.config/autostart/sparklepony.desktop
chmod 775 ~/.config/autostart/sparklepony.desktop
cp data/sparkleshare.desktop.in ~/.config/autostart/sparkleshare.desktop
chmod 775 ~/.config/autostart/sparkleshare.desktop
gtk-update-icon-cache /usr/share/icons/hicolor -f
uninstall:
rm /usr/local/bin/sparklepony
rm /usr/local/share/sparklepony/SparklePony.exe
rmdir /usr/local/share/sparklepony
rm /usr/share/icons/hicolor/*x*/places/folder-sparklepony.png
rm /usr/local/bin/sparkleshare
rm /usr/local/share/sparkleshare/SparkleShare.exe
rmdir /usr/local/share/sparkleshare
rm /usr/share/icons/hicolor/*x*/places/folder-sparkleshare.png
rm /usr/share/icons/hicolor/*x*/status/document-*ed.png
rm /usr/share/icons/hicolor/*x*/status/avatar-default.png
rm ~/.config/autostart/sparklepony.desktop
rm ~/.config/autostart/sparkleshare.desktop
clean:
rm src/SparklePony.exe
rm src/sparkleshare.exe

24
README
View file

@ -1,29 +1,29 @@
SparklePony is a file sharing and collaboration tool inspired by DropBox. It
SparkleShare is a file sharing and collaboration tool inspired by DropBox. It
allows you to instantly sync with any Git repository you have access to.
SparklePony isn't just a piece of software, it's part of your workflow. It's
SparkleShare isn't just a piece of software, it's part of your workflow. It's
designed to make sharing documents and collaboration easier, and to make peers
aware of what you are doing. The user interface and features are made to
support this goal. However, you may find SparklePony useful for other kinds of
support this goal. However, you may find SparkleShare useful for other kinds of
purposes as well, like backing up files or monitoring your favourite project.
SparklePony is not designed to be:
SparkleShare is not designed to be:
- a graphical frontend for git
- a backup tool
In contrast to the projects name, we will very likely
refuse to implement your personal ponies. :)
SparklePony currently only works on Linux/GNOME.
SparkleShare currently only works on Linux/GNOME.
Windows and OSX ports are planned for the future.
SparklePony is free software and licensed under the GNU GPLv3. You are welcome
SparkleShare is free software and licensed under the GNU GPLv3. You are welcome
to change and redistribute it under certain conditions. For more information
see the LICENSE file or visit http://www.gnu.org/licenses/gpl-3.0.html
SparklePony currently requires (in alphabetical order):
SparkleShare currently requires (in alphabetical order):
- ndesk-dbus-devel >= 0.6
- git >= 1.7
- gtk-sharp2 >= 2.12.7
@ -35,23 +35,23 @@ Installing dependencies on Fedora:
# yum install git gtk-sharp2 gtk-sharp2-devel mono-core notify-sharp \
notify-sharp-devel ndesk-dbus-devel
You can build and install SparklePony like this:
You can build and install SparkleShare like this:
$ make
$ su
# make install
Run the service:
$ sparklepony start
$ sparkleshare start
You can stop the service via the graphical interface or by typing:
$ sparklepony stop
$ sparkleshare stop
For help:
$ sparklepony --help
$ sparkleshare --help
The official website is:
http://www.github.com/hbons/SparklePony
http://www.github.com/hbons/SparkleShare
Have fun! :)

22
SparkleShare.sln Normal file
View file

@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 9.00
# Visual Studio 2005
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SparkleShare", "SparkleShare\SparkleShare.csproj", "{728483AA-E34B-4441-BF2C-C8BC2901E4E0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{728483AA-E34B-4441-BF2C-C8BC2901E4E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{728483AA-E34B-4441-BF2C-C8BC2901E4E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{728483AA-E34B-4441-BF2C-C8BC2901E4E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{728483AA-E34B-4441-BF2C-C8BC2901E4E0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(MonoDevelopProperties) = preSolution
name = SparkleShare
version = 0.1
StartupItem = SparkleShare\SparkleShare.csproj
EndGlobalSection
EndGlobal

25
SparkleShare.userprefs Normal file
View file

@ -0,0 +1,25 @@
<Properties>
<MonoDevelop.Ide.Workspace ActiveConfiguration="Debug" ctype="Workspace" />
<MonoDevelop.Ide.Workbench ActiveDocument="SparkleShare/SparklePonyWindow.cs" ctype="Workbench">
<Files>
<File FileName="SparkleShare/Main.cs" Line="35" Column="3" />
<File FileName="SparkleShare/Repository.cs" Line="463" Column="1" />
<File FileName="SparkleShare/SparklePonyUI.cs" Line="18" Column="21" />
<File FileName="SparkleShare/SparklePonyStatusIcon.cs" Line="18" Column="21" />
<File FileName="SparkleShare/SparklePonyWindow.cs" Line="490" Column="22" />
</Files>
<Pads>
<Pad Id="ProjectPad">
<State expanded="True">
<Node name="SparkleShare" expanded="True">
<Node name="References" expanded="True" />
<Node name="SparklePonyWindow.cs" selected="True" />
</Node>
</State>
</Pad>
<Pad Id="ClassPad">
<State expanded="True" selected="True" />
</Pad>
</Pads>
</MonoDevelop.Ide.Workbench>
</Properties>

2
SparkleShare.usertasks Normal file
View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<ArrayOfUserTask xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" />

106
SparkleShare/Main.cs Normal file
View file

@ -0,0 +1,106 @@
// SparkleShare, an instant update workflow to Git.
// Copyright (C) 2010 Hylke Bons <hylkebons@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
using Gtk;
using Notifications;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Timers;
namespace SparkleShare {
// This is SparkleShare!
public class SparkleShare {
public static SparkleShareUI SparkleShareUI;
public static void Main (string [] args) {
// Check if git is installed
Process Process = new Process();
Process.StartInfo.RedirectStandardOutput = true;
Process.StartInfo.UseShellExecute = false;
Process.StartInfo.FileName = "git";
Process.Start();
if (Process.StandardOutput.ReadToEnd().IndexOf ("version") == -1) {
Console.WriteLine ("Git wasn't found.");
Console.WriteLine ("You can get it from http://git-scm.com/.");
Environment.Exit (0);
}
// Don't allow running as root
Process.StartInfo.FileName = "whoami";
Process.Start();
if (Process.StandardOutput.ReadToEnd().Trim ().Equals ("root")) {
Console.WriteLine ("Sorry, you can't run SparkleShare as root.");
Console.WriteLine ("Things will go utterly wrong.");
Environment.Exit (0);
}
// Parse the command line arguments
bool HideUI = false;
if (args.Length > 0) {
foreach (string Argument in args) {
if (Argument.Equals ("--disable-gui") || Argument.Equals ("-d"))
HideUI = true;
if (Argument.Equals ("--help") || Argument.Equals ("-h")) {
ShowHelp ();
}
}
}
Gtk.Application.Init ();
SparkleShareUI = new SparkleShareUI (HideUI);
SparkleShareUI.StartMonitoring ();
Gtk.Application.Run ();
}
public static void ShowHelp () {
Console.WriteLine ("SparkleShare Copyright (C) 2010 Hylke Bons");
Console.WriteLine ("");
Console.WriteLine ("This program comes with ABSOLUTELY NO WARRANTY.");
Console.WriteLine ("This is free software, and you are welcome to redistribute it ");
Console.WriteLine ("under certain conditions. Please read the GNU GPLv3 for details.");
Console.WriteLine ("");
Console.WriteLine ("SparkleShare syncs the ~/SparkleShare folder with remote repositories.");
Console.WriteLine ("");
Console.WriteLine ("Usage: sparkleshare [start|stop|restart] [OPTION]...");
Console.WriteLine ("Sync SparkleShare folder with remote repositories.");
Console.WriteLine ("");
Console.WriteLine ("Arguments:");
Console.WriteLine ("\t -d, --disable-gui\tDon't show the notification icon.");
Console.WriteLine ("\t -h, --help\t\tDisplay this help text.");
Console.WriteLine ("");
Environment.Exit (0);
}
}
}

464
SparkleShare/Repository.cs Normal file
View file

@ -0,0 +1,464 @@
// SparkleShare, an instant update workflow to Git.
// Copyright (C) 2010 Hylke Bons <hylkebons@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
using Gtk;
using Notifications;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Timers;
namespace SparkleShare {
// Repository class holds repository information and timers
public class Repository {
private Process Process;
private Timer FetchTimer;
private Timer BufferTimer;
private FileSystemWatcher Watcher;
public string Name;
public string Domain;
public string LocalPath;
public string RemoteOriginUrl;
public string CurrentHash;
public string UserEmail;
public string UserName;
public bool MonitorOnly;
public Repository (string Path) {
MonitorOnly = false;
Process = new Process();
Process.EnableRaisingEvents = false;
Process.StartInfo.RedirectStandardOutput = true;
Process.StartInfo.UseShellExecute = false;
// Get the repository's path, example: "/home/user/SparkleShare/repo/"
LocalPath = Path;
Process.StartInfo.WorkingDirectory = LocalPath + "/";
// Get user.name, example: "User Name"
UserName = "Anonymous";
Process.StartInfo.FileName = "git";
Process.StartInfo.Arguments = "config --get user.name";
Process.Start();
UserName = Process.StandardOutput.ReadToEnd().Trim ();
// Get user.email, example: "user@github.com"
UserEmail = "not.set@git-scm.com";
Process.StartInfo.FileName = "git";
Process.StartInfo.Arguments = "config --get user.email";
Process.Start();
UserEmail = Process.StandardOutput.ReadToEnd().Trim ();
// Get remote.origin.url, example: "ssh://git@github.com/user/repo"
Process.StartInfo.FileName = "git";
Process.StartInfo.Arguments = "config --get remote.origin.url";
Process.Start();
RemoteOriginUrl = Process.StandardOutput.ReadToEnd().Trim ();
// Get the repository name, example: "Project"
string s = LocalPath.TrimEnd ( "/".ToCharArray ());
Name = LocalPath.Substring (s.LastIndexOf ("/") + 1);
// Get the domain, example: "github.com"
Domain = RemoteOriginUrl;
Domain = Domain.Substring (Domain.IndexOf ("@") + 1);
if (Domain.IndexOf (":") > -1)
Domain = Domain.Substring (0, Domain.IndexOf (":"));
else
Domain = Domain.Substring (0, Domain.IndexOf ("/"));
// Get hash of the current commit
Process.StartInfo.FileName = "git";
Process.StartInfo.Arguments = "rev-list --max-count=1 HEAD";
Process.Start();
CurrentHash = Process.StandardOutput.ReadToEnd().Trim ();
// Watch the repository's folder
Watcher = new FileSystemWatcher (LocalPath);
Watcher.IncludeSubdirectories = true;
Watcher.EnableRaisingEvents = true;
Watcher.Filter = "*";
Watcher.Changed += new FileSystemEventHandler(OnFileActivity);
Watcher.Created += new FileSystemEventHandler(OnFileActivity);
Watcher.Deleted += new FileSystemEventHandler(OnFileActivity);
// Fetch remote changes every 20 seconds
FetchTimer = new Timer ();
FetchTimer.Interval = 20000;
FetchTimer.Elapsed += delegate {
Fetch ();
};
FetchTimer.Start();
BufferTimer = new Timer ();
// Add everything that changed
// since SparkleShare was stopped
Add ();
}
// Starts a time buffer when something changes
public void OnFileActivity (object o, FileSystemEventArgs args) {
WatcherChangeTypes wct = args.ChangeType;
if (!ShouldIgnore (args.Name) && !MonitorOnly) {
Console.WriteLine("[Event][" + Name + "] " + wct.ToString() +
" '" + args.Name + "'");
StartBufferTimer ();
}
}
// A buffer that will fetch changes after
// file activity has settles down
public void StartBufferTimer () {
int Interval = 2000;
if (!BufferTimer.Enabled) {
// Delay for a few seconds to see if more files change
BufferTimer.Interval = Interval;
BufferTimer.Elapsed += delegate (object o, ElapsedEventArgs args) {
Console.WriteLine ("[Buffer][" + Name + "] Done waiting.");
Add ();
};
Console.WriteLine ("[Buffer][" + Name + "] " +
"Waiting for more changes...");
BufferTimer.Start();
} else {
// Extend the delay when something changes
BufferTimer.Close ();
BufferTimer = new Timer ();
BufferTimer.Interval = Interval;
BufferTimer.Elapsed += delegate (object o, ElapsedEventArgs args) {
Console.WriteLine ("[Buffer][" + Name + "] Done waiting.");
Add ();
};
BufferTimer.Start();
Console.WriteLine ("[Buffer][" + Name + "] " +
"Waiting for more changes...");
}
}
// Clones a remote repo
public void Clone () {
Process.StartInfo.Arguments = "clone " + RemoteOriginUrl;
Process.Start();
// Add a gitignore file
TextWriter Writer = new StreamWriter(LocalPath + ".gitignore");
Writer.WriteLine("*~"); // Ignore gedit swap files
Writer.WriteLine(".*.sw?"); // Ignore vi swap files
Writer.Close();
}
// Stages the made changes
public void Add () {
BufferTimer.Stop ();
Console.WriteLine ("[Git][" + Name + "] Staging changes...");
Process.StartInfo.Arguments = "add --all";
Process.Start();
string Message = FormatCommitMessage ();
if (!Message.Equals ("")) {
Commit (Message);
Push ();
Fetch ();
// Push again in case of a conflict
Push ();
}
}
// Commits the made changes
public void Commit (string Message) {
Console.WriteLine ("[Commit][" + Name + "] " + Message);
Console.WriteLine ("[Git][" + Name + "] Commiting changes...");
Process.StartInfo.Arguments = "commit -m \"" + Message + "\"";
Process.Start();
ShowEventNotification (UserName + " " + Message,
GetAvatarFileName (UserEmail, 48), true);
}
// Fetches changes from the remote repo
public void Fetch () {
// TODO: change status icon to sync
FetchTimer.Stop ();
Console.WriteLine ("[Git][" + Name + "] Fetching changes...");
Process.StartInfo.Arguments = "fetch";
Process.Start();
Process.WaitForExit ();
Merge ();
FetchTimer.Start ();
}
// Merges the fetched changes
public void Merge () {
Watcher.EnableRaisingEvents = false;
Console.WriteLine ("[Git][" + Name + "] Merging fetched changes...");
Process.StartInfo.Arguments = "merge origin/master";
Process.Start();
Process.WaitForExit ();
string Output = Process.StandardOutput.ReadToEnd().Trim ();
// Show notification if there are updates
if (!Output.Equals ("Already up-to-date.")) {
// Get the last commit message
Process.StartInfo.Arguments = "log --format=\"%ae\" -1";
Process.Start();
string LastCommitEmail = Process.StandardOutput.ReadToEnd().Trim ();
// Get the last commit message
Process.StartInfo.Arguments = "log --format=\"%s\" -1";
Process.Start();
string LastCommitMessage = Process.StandardOutput.ReadToEnd().Trim ();
// Get the last commiter
Process.StartInfo.Arguments = "log --format=\"%an\" -1";
Process.Start();
string LastCommitUserName = Process.StandardOutput.ReadToEnd().Trim ();
ShowEventNotification (LastCommitUserName + " " + LastCommitMessage,
GetAvatarFileName (LastCommitEmail, 48), true);
}
Watcher.EnableRaisingEvents = true;
// TODO: change status icon to normal
}
// Pushes the changes to the remote repo
public void Push () {
// TODO: What happens when network disconnects during a push
Console.WriteLine ("[Git][" + Name + "] Pushing changes...");
Process.StartInfo.Arguments = "push";
Process.Start();
Process.WaitForExit ();
}
// Ignores Repos, dotfiles, swap files and the like.
public bool ShouldIgnore (string FileName) {
if (FileName.Substring (0, 1).Equals (".") ||
FileName.Contains (".lock") ||
FileName.Contains (".git") ||
FileName.Contains ("/.") ||
Directory.Exists (LocalPath + FileName))
return true; // Yes, ignore it.
else if (FileName.Length > 3 &&
FileName.Substring (FileName.Length - 4).Equals (".swp"))
return true;
else return false;
}
// Creates a pretty commit message based on what has changed
public string FormatCommitMessage () {
bool DoneAddCommit = false;
bool DoneEditCommit = false;
bool DoneRenameCommit = false;
bool DoneDeleteCommit = false;
int FilesAdded = 0;
int FilesEdited = 0;
int FilesRenamed = 0;
int FilesDeleted = 0;
Process.StartInfo.Arguments = "status";
Process.Start();
string Output = Process.StandardOutput.ReadToEnd();
foreach (string Line in Regex.Split (Output, "\n")) {
if (Line.IndexOf ("new file:") > -1)
FilesAdded++;
if (Line.IndexOf ("modified:") > -1)
FilesEdited++;
if (Line.IndexOf ("renamed:") > -1)
FilesRenamed++;
if (Line.IndexOf ("deleted:") > -1)
FilesDeleted++;
}
foreach (string Line in Regex.Split (Output, "\n")) {
// Format message for when files are added,
// example: "added 'file' and 3 more."
if (Line.IndexOf ("new file:") > -1 && !DoneAddCommit) {
DoneAddCommit = true;
if (FilesAdded > 1)
return "added " +
Line.Replace ("#\tnew file:", "").Trim () +
" and " + (FilesAdded - 1) + " more.";
else
return "added " +
Line.Replace ("#\tnew file:", "").Trim () + ".";
}
// Format message for when files are edited,
// example: "edited 'file'."
if (Line.IndexOf ("modified:") > -1 && !DoneEditCommit) {
DoneEditCommit = true;
if (FilesEdited > 1)
return "edited " +
Line.Replace ("#\tmodified:", "").Trim () +
" and " + (FilesEdited - 1) + " more.";
else
return "edited " +
Line.Replace ("#\tmodified:", "").Trim () + ".";
}
// Format message for when files are edited,
// example: "deleted 'file'."
if (Line.IndexOf ("deleted:") > -1 && !DoneDeleteCommit) {
DoneDeleteCommit = true;
if (FilesDeleted > 1)
return "deleted " +
Line.Replace ("#\tdeleted:", "").Trim () +
" and " + (FilesDeleted - 1) + " more.";
else
return "deleted " +
Line.Replace ("#\tdeleted:", "").Trim () + ".";
}
// Format message for when files are renamed,
// example: "renamed 'file' to 'new name'."
if (Line.IndexOf ("renamed:") > -1 && !DoneRenameCommit) {
DoneDeleteCommit = true;
if (FilesRenamed > 1)
return "renamed " +
Line.Replace ("#\trenamed:", "").Trim ().Replace
(" -> ", " to ") + " and " + (FilesDeleted - 1) +
" more.";
else
return "renamed " +
Line.Replace ("#\trenamed:", "").Trim ().Replace
(" -> ", " to ") + ".";
}
}
// Nothing happened:
return "";
}
// Shows a notification with text and image
public void ShowEventNotification (string Title,
string IconFileName,
bool ShowButtons) {
Notification Notification = new Notification (Title, " ");
Notification.Urgency = Urgency.Low;
Notification.Timeout = 4500;
Notification.Icon = new Gdk.Pixbuf (IconFileName);
// Add a button to open the folder where the changed file is
if (ShowButtons)
Notification.AddAction ("", "Open Folder",
delegate (object o, ActionArgs args) {
Process.StartInfo.FileName = "xdg-open";
Process.StartInfo.Arguments = LocalPath;
Process.Start();
Process.StartInfo.FileName = "git";
} );
Notification.Show ();
}
public static string GetAvatarFileName (string Email, int Size) {
string AvatarPath = Environment.GetEnvironmentVariable("HOME") +
"/.config/sparkleshare/avatars/" +
Size + "x" + Size + "/";
if (!Directory.Exists (AvatarPath)) {
Directory.CreateDirectory (AvatarPath);
Console.WriteLine ("[Config] Created '" + AvatarPath + "'");
}
string AvatarFile = AvatarPath + Email;
if (File.Exists (AvatarFile))
return AvatarFile;
else {
// Let's try to get the person's gravatar for next time
WebClient WebClient = new WebClient ();
Uri GravatarUri = new Uri ("http://www.gravatar.com/avatar/" +
GetMD5 (Email) + ".jpg?s=" + Size + "&d=404");
string TmpFile = "/tmp/" + Email + Size;
if (!File.Exists (TmpFile)) {
WebClient.DownloadFileAsync (GravatarUri, TmpFile);
WebClient.DownloadFileCompleted += delegate {
File.Delete (AvatarPath + Email);
FileInfo TmpFileInfo = new FileInfo (TmpFile);
if (TmpFileInfo.Length > 255)
File.Move (TmpFile, AvatarPath + Email);
};
}
string FallbackFileName = "/usr/share/icons/hicolor/" +
Size + "x" + Size +
"/status/avatar-default.png";
if (File.Exists (FallbackFileName))
return FallbackFileName;
else
return "/usr/share/icons/hicolor/16x16/status/avatar-default.png";
}
}
// Helper that creates an MD5 hash
public static string GetMD5 (string s) {
MD5 md5 = new MD5CryptoServiceProvider ();
Byte[] Bytes = ASCIIEncoding.Default.GetBytes (s);
Byte[] EncodedBytes = md5.ComputeHash (Bytes);
return BitConverter.ToString(EncodedBytes).ToLower ().Replace ("-", "");
}
}
}

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>8.0.50727</ProductVersion>
<ProjectGuid>{728483AA-E34B-4441-BF2C-C8BC2901E4E0}</ProjectGuid>
<OutputType>Exe</OutputType>
<AssemblyName>SparkleShare</AssemblyName>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug</OutputPath>
<DefineConstants>DEBUG</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>none</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Release</OutputPath>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<ItemGroup>
<Reference Include="gtk-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f" />
<Reference Include="notify-sharp, Version=0.4.0.0, Culture=neutral, PublicKeyToken=2df29c54e245917a" />
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<Compile Include="Main.cs" />
<Compile Include="Repository.cs" />
<Compile Include="SparkleShareUI.cs" />
<Compile Include="SparkleShareStatusIcon.cs" />
<Compile Include="SparkleShareWindow.cs" />
</ItemGroup>
</Project>

Binary file not shown.

View file

@ -0,0 +1,67 @@
// SparkleShare, an instant update workflow to Git.
// Copyright (C) 2010 Hylke Bons <hylkebons@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
using Gtk;
using Notifications;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Timers;
namespace SparkleShare {
public class SparkleShareStatusIcon : StatusIcon {
public SparkleShareStatusIcon () : base () {
IconName = "folder-sparkleshare";
string UserHome = Environment.GetEnvironmentVariable("HOME") + "/";
string FirstRunFile = UserHome + ".config/sparkleshare/firstrun";
// Show a notification on the first run
if (File.Exists (FirstRunFile)) {
Notification Notification;
Notification = new Notification ("Welcome to SparkleShare!",
"Click here to add some folders.");
Notification.Urgency = Urgency.Normal;
Notification.Timeout = 7500;
Notification.Show ();
File.Delete (FirstRunFile);
Console.WriteLine ("[Config] Deleted '" + FirstRunFile + "'");
}
}
public void SetIdleState () {
IconName = "folder-sparkleshare";
}
public void SetSyncingState () {
IconName = "view-refresh"; // Massively abusing this icon here :)
}
}
}

View file

@ -0,0 +1,117 @@
// SparkleShare, an instant update workflow to Git.
// Copyright (C) 2010 Hylke Bons <hylkebons@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
using Gtk;
using Notifications;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Timers;
namespace SparkleShare {
// Holds the status icon, window and repository list
public class SparkleShareUI {
public SparkleShareWindow SparkleShareWindow;
public SparkleShareStatusIcon SparkleShareStatusIcon;
public Repository [] Repositories;
public SparkleShareUI (bool HideUI) {
Process Process = new Process();
Process.EnableRaisingEvents = false;
Process.StartInfo.RedirectStandardOutput = true;
Process.StartInfo.UseShellExecute = false;
// Get home folder, example: "/home/user/"
string UserHome = Environment.GetEnvironmentVariable("HOME") + "/";
// Create 'SparkleShare' folder in the user's home folder
string ReposPath = UserHome + "SparkleShare";
if (!Directory.Exists (ReposPath)) {
Directory.CreateDirectory (ReposPath);
Console.WriteLine ("[Config] Created '" + ReposPath + "'");
Process.StartInfo.FileName = "gvfs-set-attribute";
Process.StartInfo.Arguments = ReposPath + " metadata::custom-icon " +
"folder-sparkleshare";
Process.Start();
}
// Create place to store configuration user's home folder
string ConfigPath = UserHome + ".config/sparkleshare/";
if (!Directory.Exists (ConfigPath)) {
Directory.CreateDirectory (ConfigPath);
Console.WriteLine ("[Config] Created '" + ConfigPath + "'");
// Create a first run file to show the intro message
File.Create (ConfigPath + "firstrun");
Console.WriteLine ("[Config] Created '" + ConfigPath + "firstrun'");
// Create a place to store the avatars
Directory.CreateDirectory (ConfigPath + "avatars/");
Console.WriteLine ("[Config] Created '" + ConfigPath + "avatars'");
}
// Get all the repos in ~/SparkleShare
string [] Repos = Directory.GetDirectories (ReposPath);
Repositories = new Repository [Repos.Length];
int i = 0;
foreach (string Folder in Repos) {
Repositories [i] = new Repository (Folder);
i++;
}
// Don't create the window and status
// icon when --disable-gui was given
if (!HideUI) {
// Create the window
SparkleShareWindow = new SparkleShareWindow (Repositories);
SparkleShareWindow.DeleteEvent += CloseSparkleShareWindow;
// Create the status icon
SparkleShareStatusIcon = new SparkleShareStatusIcon ();
SparkleShareStatusIcon.Activate += delegate {
SparkleShareWindow.ToggleVisibility ();
};
}
}
// Closes the window
public void CloseSparkleShareWindow (object o, DeleteEventArgs args) {
SparkleShareWindow = new SparkleShareWindow (Repositories);
SparkleShareWindow.DeleteEvent += CloseSparkleShareWindow;
}
public void StartMonitoring () { }
public void StopMonitoring () { }
}
}

View file

@ -0,0 +1,506 @@
// SparkleShare, an instant update workflow to Git.
// Copyright (C) 2010 Hylke Bons <hylkebons@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
using Gtk;
using Notifications;
using SparkleShare;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Timers;
namespace SparkleShare {
public class SparkleShareWindow : Window {
private bool Visibility;
private VBox LayoutVerticalLeft;
private VBox LayoutVerticalRight;
private HBox LayoutHorizontal;
private TreeView ReposView;
private ListStore ReposStore;
private Repository [] Repositories;
public SparkleShareWindow (Repository [] R) : base ("SparkleShare") {
Repositories = R;
Visibility = false;
SetSizeRequest (720, 540);
SetPosition (WindowPosition.Center);
BorderWidth = 6;
IconName = "folder-sparkleshare";
VBox LayoutVertical = new VBox (false, 0);
Notebook Notebook = new Notebook ();
Notebook.BorderWidth = 6;
LayoutHorizontal = new HBox (false, 0);
ReposStore = new ListStore (typeof (Gdk.Pixbuf),
typeof (string),
typeof (Repository));
LayoutVerticalLeft = CreateReposList ();
LayoutVerticalLeft.BorderWidth = 12;
LayoutVerticalRight = CreateDetailedView (Repositories [1]);
LayoutHorizontal.PackStart (LayoutVerticalLeft, false, false, 0);
LayoutHorizontal.PackStart (LayoutVerticalRight, true, true, 12);
Notebook.AppendPage (LayoutHorizontal, new Label ("Folders"));
Notebook.AppendPage (CreateEventLog (), new Label ("Events"));
LayoutVertical.PackStart (Notebook, true, true, 0);
HButtonBox DialogButtons = new HButtonBox ();
DialogButtons.BorderWidth = 6;
Button QuitServiceButton = new Button ("Quit Service");
QuitServiceButton.Clicked += Quit;
Button CloseButton = new Button (Stock.Close);
CloseButton.Clicked += delegate (object o, EventArgs args) {
Visibility = false;
HideAll ();
};
DialogButtons.Add (QuitServiceButton);
DialogButtons.Add (CloseButton);
LayoutVertical.PackStart (DialogButtons, false, false, 0);
// Fetch remote changes every 20 seconds
Timer RedrawTimer = new Timer ();
RedrawTimer.Interval = 5000;
RedrawTimer.Elapsed += delegate {
TreeSelection Selection = ReposView.Selection;;
TreeIter Iter = new TreeIter ();;
Selection.GetSelected (out Iter);
Repository Repository = (Repository)ReposStore.GetValue (Iter, 2);
Console.WriteLine(Repository.Name);
LayoutHorizontal.Remove (LayoutVerticalRight);
LayoutVerticalRight = CreateDetailedView (Repository);
LayoutHorizontal.PackStart (LayoutVerticalRight, true, true, 12);
ShowAll ();
};
RedrawTimer.Start();
Add (LayoutVertical);
}
// Creates a visual list of repositories
public VBox CreateReposList() {
string RemoteFolderIcon = "/usr/share/icons/gnome/32x32/places/folder.png";
TreeIter ReposIter;
foreach (Repository Repository in Repositories) {
ReposIter = ReposStore.Prepend ();
ReposStore.SetValue (ReposIter, 0, new Gdk.Pixbuf (RemoteFolderIcon));
ReposStore.SetValue (ReposIter, 1, Repository.Name + " \n" +
Repository.Domain + " ");
ReposStore.SetValue (ReposIter, 2, Repository);
}
ScrolledWindow ScrolledWindow = new ScrolledWindow ();
ReposView = new TreeView (ReposStore);
ReposView.AppendColumn ("", new CellRendererPixbuf () , "pixbuf", 0);
ReposView.AppendColumn ("", new Gtk.CellRendererText (), "text", 1);
TreeViewColumn [] ReposViewColumns = ReposView.Columns;
ReposViewColumns [0].MinWidth = 48;
ReposView.HeadersVisible = false;
ReposStore.IterNthChild (out ReposIter, 0);
ReposView.ActivateRow (ReposStore.GetPath (ReposIter),
ReposViewColumns [1]);
ReposView.CursorChanged += delegate {
TreeSelection Selection = ReposView.Selection;;
TreeIter Iter = new TreeIter ();;
Selection.GetSelected (out Iter);
Repository Repository = (Repository)ReposStore.GetValue (Iter, 2);
Console.WriteLine(Repository.Name);
LayoutHorizontal.Remove (LayoutVerticalRight);
LayoutVerticalRight = CreateDetailedView (Repository);
LayoutHorizontal.PackStart (LayoutVerticalRight, true, true, 12);
ShowAll ();
};
HBox AddRemoveButtons = new HBox (false, 6);
Button AddButton = new Button ("Add...");
AddRemoveButtons.PackStart (AddButton, true, true, 0);
Image RemoveImage = new Image ("/usr/share/icons/gnome/16x16/actions/list-remove.png");
Button RemoveButton = new Button ();
RemoveButton.Image = RemoveImage;
AddRemoveButtons.PackStart (RemoveButton, false, false, 0);
ScrolledWindow.AddWithViewport (ReposView);
ScrolledWindow.WidthRequest = 200;
VBox VBox = new VBox (false, 6);
VBox.PackStart (ScrolledWindow, true, true, 0);
VBox.PackStart (AddRemoveButtons, false, false, 0);
return VBox;
}
// Creates the detailed view
public VBox CreateDetailedView (Repository Repository) {
Console.WriteLine ("repo: " + Repository.Name);
// Create box layout for remote url
HBox RemoteUrlBox = new HBox (false, 0);
Label Property1 = new Label ("Remote URL:");
Property1.WidthRequest = 120;
Property1.SetAlignment (0, 0);
Label Value1 = new Label
("<b>" + Repository.RemoteOriginUrl + "</b>");
Value1.UseMarkup = true;
RemoteUrlBox.PackStart (Property1, false, false, 0);
RemoteUrlBox.PackStart (Value1, false, false, 0);
// Create box layout for repository path
HBox LocalPathBox = new HBox (false, 0);
Label Property2 = new Label ("Local path:");
Property2.WidthRequest = 120;
Property2.SetAlignment (0, 0);
Label Value2 = new Label
("<b>" + Repository.LocalPath + "</b>");
Value2.UseMarkup = true;
LocalPathBox.PackStart (Property2, false, false, 0);
LocalPathBox.PackStart (Value2, false, false, 0);
CheckButton NotificationsCheckButton =
new CheckButton ("Notify me when something changes");
NotificationsCheckButton.Active = true;
CheckButton ChangesCheckButton =
new CheckButton ("Synchronize my changes");
ChangesCheckButton.Active = true;
Table Table = new Table(7, 2, false);
Table.RowSpacing = 6;
Table.Attach(RemoteUrlBox, 0, 2, 0, 1);
Table.Attach(LocalPathBox, 0, 2, 1, 2);
Table.Attach(NotificationsCheckButton, 0, 2, 4, 5);
Table.Attach(ChangesCheckButton, 0, 2, 5, 6);
VBox VBox = new VBox (false, 0);
VBox.PackStart (Table, false, false, 12);
Label PeopleLabel =
new Label ("<span font_size='large'><b>Active users" +
"</b></span>");
PeopleLabel.UseMarkup = true;
PeopleLabel.SetAlignment (0, 0);
VBox.PackStart (PeopleLabel, false, false, 0);
VBox.PackStart
(CreatePeopleList (Repository ), true, true, 12);
return VBox;
}
public ScrolledWindow CreateEventLog() {
ListStore LogStore = new ListStore (typeof (Gdk.Pixbuf),
typeof (string),
typeof (string));
Process Process = new Process();
Process.EnableRaisingEvents = false;
Process.StartInfo.RedirectStandardOutput = true;
Process.StartInfo.UseShellExecute = false;
Process.StartInfo.FileName = "git";
string Output = "";
foreach (Repository Repository in Repositories) {
// We're using the snowman here to separate messages :)
Process.StartInfo.Arguments =
"log --format=\"%at☃In " + Repository.Name + ", %an %s☃%cr\" -25";
Process.StartInfo.WorkingDirectory = Repository.LocalPath;
Process.Start();
Output += "\n" + Process.StandardOutput.ReadToEnd().Trim ();
}
Output = Output.TrimStart ("\n".ToCharArray ());
string [] Lines = Regex.Split (Output, "\n");
// Sort by time and get the last 25
Array.Sort (Lines);
Array.Reverse (Lines);
string [] LastTwentyFive = new string [25];
Array.Copy (Lines, 0, LastTwentyFive, 0, 25);
TreeIter Iter;
foreach (string Line in LastTwentyFive) {
// Look for the snowman!
string [] Parts = Regex.Split (Line, "☃");
string Message = Parts [1];
string TimeAgo = Parts [2];
string IconFile =
"/usr/share/icons/hicolor/16x16/status/document-edited.png";
if (Message.IndexOf (" added ") > -1)
IconFile =
"/usr/share/icons/hicolor/16x16/status/document-added.png";
if (Message.IndexOf (" deleted ") > -1)
IconFile =
"/usr/share/icons/hicolor/16x16/status/document-removed.png";
if (Message.IndexOf (" moved ") > -1 ||
Message.IndexOf (" renamed ") > -1)
IconFile =
"/usr/share/icons/hicolor/16x16/status/document-moved.png";
Iter = LogStore.Append ();
LogStore.SetValue (Iter, 0, new Gdk.Pixbuf (IconFile));
LogStore.SetValue (Iter, 1, Message);
// TODO: right align time
LogStore.SetValue (Iter, 2, " " + TimeAgo);
}
TreeView LogView = new TreeView (LogStore);
LogView.HeadersVisible = false;
CellRendererText TextCellRight = new Gtk.CellRendererText ();
TextCellRight.Alignment = Pango.Alignment.Right;
LogView.AppendColumn ("", new Gtk.CellRendererPixbuf (), "pixbuf", 0);
LogView.AppendColumn ("", new Gtk.CellRendererText (), "text", 1);
LogView.AppendColumn ("", TextCellRight, "text", 2);
TreeViewColumn [] Columns = LogView.Columns;
Columns [0].MinWidth = 32;
Columns [1].Expand = true;
Columns [1].MaxWidth = 150;
ScrolledWindow ScrolledWindow = new ScrolledWindow ();
ScrolledWindow.AddWithViewport (LogView);
ScrolledWindow.BorderWidth = 12;
return ScrolledWindow;
}
// Creates a visual list of people working in the repo
public ScrolledWindow CreatePeopleList (Repository Repository) {
Process Process = new Process ();
Process.EnableRaisingEvents = false;
Process.StartInfo.RedirectStandardOutput = true;
Process.StartInfo.UseShellExecute = false;
// Get a log of commits, example: "Hylke Bons☃added 'file'."
Process.StartInfo.FileName = "git";
Process.StartInfo.Arguments = "log --format=\"%an☃%ae\" -50";
Process.StartInfo.WorkingDirectory = Repository.LocalPath;
Process.Start();
string Output = Process.StandardOutput.ReadToEnd().Trim ();
string [] People = new string [50];
string [] Lines = Regex.Split (Output, "\n");
ListStore PeopleStore = new ListStore (typeof (Gdk.Pixbuf),
typeof (string));
TreeIter PeopleIter;
int i = 0;
foreach (string Line in Lines) {
// Only add name if it isn't there already
if (Array.IndexOf (People, Line) == -1) {
People [i] = Line;
string [] Parts = Regex.Split (Line, "☃");
string UserName = Parts [0];
string UserEmail = Parts [1];
// Do something special if the person is you
if (UserName.Equals (Repository.UserName))
UserName += " (thats you!)";
// Actually add to the list
PeopleIter = PeopleStore.Prepend ();
PeopleStore.SetValue (PeopleIter, 0, new Gdk.Pixbuf (GetAvatarFileName (UserEmail, 32)));
PeopleStore.SetValue (PeopleIter, 1, UserName + "\n" + UserEmail);
}
i++;
}
TreeView PeopleView = new TreeView (PeopleStore);
PeopleView.AppendColumn ("", new CellRendererPixbuf () , "pixbuf", 0);
PeopleView.AppendColumn ("", new Gtk.CellRendererText (), "text", 1);
TreeViewColumn [] PeopleViewColumns = PeopleView.Columns;
PeopleViewColumns [0].MinWidth = 48;
PeopleViewColumns [1].Expand = true;
PeopleView.HeadersVisible = false;
ScrolledWindow ScrolledWindow = new ScrolledWindow ();
ScrolledWindow.AddWithViewport (PeopleView);
return ScrolledWindow;
}
public void UpdatePeopleList () {
}
public void ToggleVisibility() {
Present ();
if (Visibility) {
if (HasFocus)
HideAll ();
} else {
ShowAll ();
}
}
public void Quit (object o, EventArgs args) {
File.Delete ("/tmp/sparkleshare/sparkleshare.pid");
Application.Quit ();
}
public static string GetAvatarFileName (string Email, int Size) {
string AvatarPath = Environment.GetEnvironmentVariable("HOME") +
"/.config/sparkleshare/avatars/" +
Size + "x" + Size + "/";
if (!Directory.Exists (AvatarPath)) {
Directory.CreateDirectory (AvatarPath);
Console.WriteLine ("[Config] Created '" + AvatarPath + "'");
}
string AvatarFile = AvatarPath + Email;
if (File.Exists (AvatarFile))
return AvatarFile;
else {
// Let's try to get the person's gravatar for next time
WebClient WebClient = new WebClient ();
Uri GravatarUri = new Uri ("http://www.gravatar.com/avatar/" +
GetMD5 (Email) + ".jpg?s=" + Size + "&d=404");
string TmpFile = "/tmp/" + Email + Size;
if (!File.Exists (TmpFile)) {
WebClient.DownloadFileAsync (GravatarUri, TmpFile);
WebClient.DownloadFileCompleted += delegate {
File.Delete (AvatarPath + Email);
FileInfo TmpFileInfo = new FileInfo (TmpFile);
if (TmpFileInfo.Length > 255)
File.Move (TmpFile, AvatarPath + Email);
};
}
string FallbackFileName = "/usr/share/icons/hicolor/" +
Size + "x" + Size +
"/status/avatar-default.png";
if (File.Exists (FallbackFileName))
return FallbackFileName;
else
return "/usr/share/icons/hicolor/16x16/status/avatar-default.png";
}
}
// Helper that creates an MD5 hash
public static string GetMD5 (string s) {
MD5 md5 = new MD5CryptoServiceProvider ();
Byte[] Bytes = ASCIIEncoding.Default.GetBytes (s);
Byte[] EncodedBytes = md5.ComputeHash (Bytes);
return BitConverter.ToString(EncodedBytes).ToLower ().Replace ("-", "");
}
}
}

View file

Before

Width:  |  Height:  |  Size: 766 B

After

Width:  |  Height:  |  Size: 766 B

View file

Before

Width:  |  Height:  |  Size: 1,004 B

After

Width:  |  Height:  |  Size: 1,004 B

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

View file

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,5 +0,0 @@
[Desktop Entry]
Name=SparklePony
Exec=sparklepony start
Icon=folder-sparklepony
Terminal=false

View file

@ -0,0 +1,5 @@
[Desktop Entry]
Name=SparkleShare
Exec=sparkleshare start
Icon=folder-sparkleshare
Terminal=false

View file

@ -16,7 +16,7 @@
inkscape:export-ydpi="90.000000"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
inkscape:version="0.46"
sodipodi:docname="sparklepony.svg"
sodipodi:docname="sparkleshare.svg"
sodipodi:version="0.32"
style="display:inline;enable-background:new"
version="1.0"
@ -163,7 +163,7 @@
<dc:title>Lapo Calamandrei</dc:title>
</cc:Agent>
</dc:creator>
<dc:title>SparklePony</dc:title>
<dc:title>sparkleshare</dc:title>
<dc:subject>
<rdf:Bag>
<rdf:li>folder</rdf:li>
@ -1832,7 +1832,7 @@
<g
inkscape:groupmode="layer"
id="layer17"
inkscape:label="artwork:sparklepony"
inkscape:label="artwork:sparkleshare"
style="display:inline"
transform="translate(0,300)">
<g
@ -1911,7 +1911,7 @@
y="-228.48639"
x="166.9707"
sodipodi:role="line"
id="tspan4716">sparklepony</tspan></text>
id="tspan4716">sparkleshare</tspan></text>
</g>
<g
inkscape:groupmode="layer"

Before

Width:  |  Height:  |  Size: 809 KiB

After

Width:  |  Height:  |  Size: 809 KiB

File diff suppressed because it is too large Load diff