├── .github └── FUNDING.yml ├── .gitignore ├── GChan.UnitTest ├── ConcurrentDictionaryHashCodeKeyTests.cs ├── GChan.UnitTest.csproj ├── Properties │ └── AssemblyInfo.cs └── packages.config ├── GChan.sln ├── GChan ├── 4chanIcon.ico ├── App.config ├── BoundListView.Designer.cs ├── BoundListView.cs ├── Controllers │ ├── Download.cs │ ├── DownloadManager.cs │ ├── MainController.cs │ └── UpdateController.cs ├── Controls │ ├── DataGridViewPreferencesSetting.cs │ ├── PreferencesDataGridView.cs │ └── SortableBindingList │ │ ├── PropertyComparer.cs │ │ └── SortableBindingList.cs ├── Data │ ├── DataController.cs │ ├── LoadedBoardData.cs │ ├── LoadedData.cs │ └── LoadedThreadData.cs ├── Forms │ ├── AboutBox.Designer.cs │ ├── AboutBox.cs │ ├── AboutBox.resx │ ├── Changelog.cs │ ├── Changelog.designer.cs │ ├── Changelog.resx │ ├── CloseWarn.cs │ ├── CloseWarn.designer.cs │ ├── CloseWarn.resx │ ├── GetStringMessageBox.Designer.cs │ ├── GetStringMessageBox.cs │ ├── GetStringMessageBox.resx │ ├── MainForm.Designer.cs │ ├── MainForm.cs │ ├── MainForm.resx │ ├── SettingsForm.cs │ ├── SettingsForm.designer.cs │ ├── SettingsForm.resx │ ├── UpdateInfoForm.Designer.cs │ ├── UpdateInfoForm.cs │ └── UpdateInfoForm.resx ├── GChan.csproj ├── Helpers │ ├── EnumHelper.cs │ ├── EnumerableExtensions.cs │ ├── ExceptionExtensions.cs │ └── Utils.cs ├── Models │ ├── ConcurrentHashSet.cs │ ├── MainFormModel.cs │ └── SavedIdsCollection.cs ├── Program.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── DataSources │ │ └── GChan.Models.MainFormModel.datasource │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings ├── System.Data.SQLite.dll ├── TodoList.txt ├── Trackers │ ├── Asset.cs │ ├── Board.cs │ ├── IDownloadable.cs │ ├── Sites │ │ ├── Board_4Chan.cs │ │ └── Thread_4Chan.cs │ ├── Thread.cs │ └── Tracker.cs ├── icons │ ├── alert.png │ ├── clipboard.png │ ├── close.png │ ├── download.png │ ├── file.png │ ├── folder.png │ ├── question.png │ ├── rename.png │ ├── settings-wrench.png │ └── world.png └── nlog.config ├── LICENSE.txt ├── README.md └── packageRelease.ps1 /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Issung 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.pch 68 | *.pdb 69 | *.pgc 70 | *.pgd 71 | *.rsp 72 | *.sbr 73 | *.tlb 74 | *.tli 75 | *.tlh 76 | *.tmp 77 | *.tmp_proj 78 | *.log 79 | *.vspscc 80 | *.vssscc 81 | .builds 82 | *.pidb 83 | *.svclog 84 | *.scc 85 | 86 | # Chutzpah Test files 87 | _Chutzpah* 88 | 89 | # Visual C++ cache files 90 | ipch/ 91 | *.aps 92 | *.ncb 93 | *.opendb 94 | *.opensdf 95 | *.sdf 96 | *.cachefile 97 | *.VC.db 98 | *.VC.VC.opendb 99 | 100 | # Visual Studio profiler 101 | *.psess 102 | *.vsp 103 | *.vspx 104 | *.sap 105 | 106 | # Visual Studio Trace Files 107 | *.e2e 108 | 109 | # TFS 2012 Local Workspace 110 | $tf/ 111 | 112 | # Guidance Automation Toolkit 113 | *.gpState 114 | 115 | # ReSharper is a .NET coding add-in 116 | _ReSharper*/ 117 | *.[Rr]e[Ss]harper 118 | *.DotSettings.user 119 | 120 | # JustCode is a .NET coding add-in 121 | .JustCode 122 | 123 | # TeamCity is a build add-in 124 | _TeamCity* 125 | 126 | # DotCover is a Code Coverage Tool 127 | *.dotCover 128 | 129 | # AxoCover is a Code Coverage Tool 130 | .axoCover/* 131 | !.axoCover/settings.json 132 | 133 | # Visual Studio code coverage results 134 | *.coverage 135 | *.coveragexml 136 | 137 | # NCrunch 138 | _NCrunch_* 139 | .*crunch*.local.xml 140 | nCrunchTemp_* 141 | 142 | # MightyMoose 143 | *.mm.* 144 | AutoTest.Net/ 145 | 146 | # Web workbench (sass) 147 | .sass-cache/ 148 | 149 | # Installshield output folder 150 | [Ee]xpress/ 151 | 152 | # DocProject is a documentation generator add-in 153 | DocProject/buildhelp/ 154 | DocProject/Help/*.HxT 155 | DocProject/Help/*.HxC 156 | DocProject/Help/*.hhc 157 | DocProject/Help/*.hhk 158 | DocProject/Help/*.hhp 159 | DocProject/Help/Html2 160 | DocProject/Help/html 161 | 162 | # Click-Once directory 163 | publish/ 164 | 165 | # Publish Web Output 166 | *.[Pp]ublish.xml 167 | *.azurePubxml 168 | # Note: Comment the next line if you want to checkin your web deploy settings, 169 | # but database connection strings (with potential passwords) will be unencrypted 170 | *.pubxml 171 | *.publishproj 172 | 173 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 174 | # checkin your Azure Web App publish settings, but sensitive information contained 175 | # in these scripts will be unencrypted 176 | PublishScripts/ 177 | 178 | # NuGet Packages 179 | *.nupkg 180 | # The packages folder can be ignored because of Package Restore 181 | **/[Pp]ackages/* 182 | # except build/, which is used as an MSBuild target. 183 | !**/[Pp]ackages/build/ 184 | # Uncomment if necessary however generally it will be regenerated when needed 185 | #!**/[Pp]ackages/repositories.config 186 | # NuGet v3's project.json files produces more ignorable files 187 | *.nuget.props 188 | *.nuget.targets 189 | 190 | # Microsoft Azure Build Output 191 | csx/ 192 | *.build.csdef 193 | 194 | # Microsoft Azure Emulator 195 | ecf/ 196 | rcf/ 197 | 198 | # Windows Store app package directories and files 199 | AppPackages/ 200 | BundleArtifacts/ 201 | Package.StoreAssociation.xml 202 | _pkginfo.txt 203 | *.appx 204 | 205 | # Visual Studio cache files 206 | # files ending in .cache can be ignored 207 | *.[Cc]ache 208 | # but keep track of directories ending in .cache 209 | !*.[Cc]ache/ 210 | 211 | # Others 212 | ClientBin/ 213 | ~$* 214 | *~ 215 | *.dbmdl 216 | *.dbproj.schemaview 217 | *.jfm 218 | *.pfx 219 | *.publishsettings 220 | orleans.codegen.cs 221 | 222 | # Including strong name files can present a security risk 223 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 224 | #*.snk 225 | 226 | # Since there are multiple workflows, uncomment next line to ignore bower_components 227 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 228 | #bower_components/ 229 | 230 | # RIA/Silverlight projects 231 | Generated_Code/ 232 | 233 | # Backup & report files from converting an old project file 234 | # to a newer Visual Studio version. Backup files are not needed, 235 | # because we have git ;-) 236 | _UpgradeReport_Files/ 237 | Backup*/ 238 | UpgradeLog*.XML 239 | UpgradeLog*.htm 240 | ServiceFabricBackup/ 241 | 242 | # SQL Server files 243 | *.mdf 244 | *.ldf 245 | *.ndf 246 | 247 | # Business Intelligence projects 248 | *.rdl.data 249 | *.bim.layout 250 | *.bim_*.settings 251 | *.rptproj.rsuser 252 | 253 | # Microsoft Fakes 254 | FakesAssemblies/ 255 | 256 | # GhostDoc plugin setting file 257 | *.GhostDoc.xml 258 | 259 | # Node.js Tools for Visual Studio 260 | .ntvs_analysis.dat 261 | node_modules/ 262 | 263 | # Visual Studio 6 build log 264 | *.plg 265 | 266 | # Visual Studio 6 workspace options file 267 | *.opt 268 | 269 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 270 | *.vbw 271 | 272 | # Visual Studio LightSwitch build output 273 | **/*.HTMLClient/GeneratedArtifacts 274 | **/*.DesktopClient/GeneratedArtifacts 275 | **/*.DesktopClient/ModelManifest.xml 276 | **/*.Server/GeneratedArtifacts 277 | **/*.Server/ModelManifest.xml 278 | _Pvt_Extensions 279 | 280 | # Paket dependency manager 281 | .paket/paket.exe 282 | paket-files/ 283 | 284 | # FAKE - F# Make 285 | .fake/ 286 | 287 | # JetBrains Rider 288 | .idea/ 289 | *.sln.iml 290 | 291 | # CodeRush 292 | .cr/ 293 | 294 | # Python Tools for Visual Studio (PTVS) 295 | __pycache__/ 296 | *.pyc 297 | 298 | # Cake - Uncomment if you are using it 299 | # tools/** 300 | # !tools/packages.config 301 | 302 | # Tabs Studio 303 | *.tss 304 | 305 | # Telerik's JustMock configuration file 306 | *.jmconfig 307 | 308 | # BizTalk build output 309 | *.btp.cs 310 | *.btm.cs 311 | *.odx.cs 312 | *.xsd.cs 313 | 314 | # OpenCover UI analysis results 315 | OpenCover/ 316 | 317 | # Azure Stream Analytics local run output 318 | ASALocalRun/ 319 | 320 | # MSBuild Binary and Structured Log 321 | *.binlog 322 | 323 | # NVidia Nsight GPU debugger configuration file 324 | *.nvuser 325 | -------------------------------------------------------------------------------- /GChan.UnitTest/ConcurrentDictionaryHashCodeKeyTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using GChan.Trackers; 3 | using System.Collections.Concurrent; 4 | using Xunit; 5 | 6 | namespace GChan.UnitTest 7 | { 8 | /// 9 | /// Tests asserting that uses our implementation to insert and retrieve values. 10 | /// 11 | public class ConcurrentDictionaryHashCodeKeyTests 12 | { 13 | readonly static Thread thread = new Thread_4Chan("https://boards.4chan.org/hr/thread/123"); 14 | 15 | // Two different image link instances, with the same data. 16 | readonly static Asset asset1 = new Asset(123, "http://test.com", "test123", 1, thread); 17 | readonly static Asset asset2 = new Asset(123, "http://test.com", "test123", 1, thread); 18 | 19 | [Fact] 20 | public void TestAddAndRetrieveWithIndexOperator() 21 | { 22 | var dict = new ConcurrentDictionary(); 23 | 24 | // Store the value using the first imagelink instance as the key. 25 | dict[asset1] = 1; 26 | 27 | // Try getting the value using the 2nd instance. 28 | dict[asset2].Should().Be(1); 29 | } 30 | 31 | [Fact] 32 | public void TestAddAndRetrieveWithTryMethods() 33 | { 34 | var dict = new ConcurrentDictionary(); 35 | 36 | // Store the value using the first imagelink instance as the key. 37 | dict.TryAdd(asset1, 1); 38 | 39 | // Try getting the value using the 2nd instance. 40 | dict.TryGetValue(asset2, out var result).Should().BeTrue(); 41 | result.Should().Be(1); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /GChan.UnitTest/GChan.UnitTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Debug 10 | AnyCPU 11 | {98DBF905-A5BF-44F8-B69D-FD94A092F397} 12 | Library 13 | Properties 14 | GChan.UnitTest 15 | GChan.UnitTest 16 | v4.8 17 | 512 18 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 19 | 15.0 20 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 21 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages 22 | False 23 | UnitTest 24 | 25 | 26 | 27 | 28 | true 29 | full 30 | false 31 | bin\Debug\ 32 | DEBUG;TRACE 33 | prompt 34 | 4 35 | 36 | 37 | pdbonly 38 | true 39 | bin\Release\ 40 | TRACE 41 | prompt 42 | 4 43 | 44 | 45 | 46 | ..\packages\FluentAssertions.6.11.0\lib\net47\FluentAssertions.dll 47 | 48 | 49 | ..\packages\Microsoft.CodeCoverage.17.7.0\lib\net462\Microsoft.VisualStudio.CodeCoverage.Shim.dll 50 | 51 | 52 | ..\packages\MSTest.TestFramework.3.0.2\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.dll 53 | 54 | 55 | ..\packages\MSTest.TestFramework.3.0.2\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll 64 | 65 | 66 | ..\packages\System.Threading.Tasks.Extensions.4.5.0\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll 67 | 68 | 69 | 70 | 71 | ..\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll 72 | True 73 | 74 | 75 | ..\packages\xunit.assert.2.5.0\lib\netstandard1.1\xunit.assert.dll 76 | 77 | 78 | ..\packages\xunit.extensibility.core.2.5.0\lib\net452\xunit.core.dll 79 | 80 | 81 | ..\packages\xunit.extensibility.execution.2.5.0\lib\net452\xunit.execution.desktop.dll 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | {b6227bcb-9008-4e3e-b1a9-018c3b175c02} 98 | GChan 99 | 100 | 101 | 102 | 103 | 104 | 105 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /GChan.UnitTest/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyTitle("GChan.UnitTest")] 6 | [assembly: AssemblyDescription("")] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("")] 9 | [assembly: AssemblyProduct("GChan.UnitTest")] 10 | [assembly: AssemblyCopyright("Copyright © 2023")] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | [assembly: Guid("98dbf905-a5bf-44f8-b69d-fd94a092f397")] 17 | 18 | // [assembly: AssemblyVersion("1.0.*")] 19 | [assembly: AssemblyVersion("1.0.0.0")] 20 | [assembly: AssemblyFileVersion("1.0.0.0")] 21 | -------------------------------------------------------------------------------- /GChan.UnitTest/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /GChan.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32922.545 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GChan", "GChan\GChan.csproj", "{B6227BCB-9008-4E3E-B1A9-018C3B175C02}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{22D80AB9-2B67-45D0-8BF0-8CC882DC71FA}" 9 | ProjectSection(SolutionItems) = preProject 10 | .gitignore = .gitignore 11 | LICENSE.txt = LICENSE.txt 12 | packageRelease.ps1 = packageRelease.ps1 13 | README.md = README.md 14 | EndProjectSection 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GChan.UnitTest", "GChan.UnitTest\GChan.UnitTest.csproj", "{98DBF905-A5BF-44F8-B69D-FD94A092F397}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {B6227BCB-9008-4E3E-B1A9-018C3B175C02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {B6227BCB-9008-4E3E-B1A9-018C3B175C02}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {B6227BCB-9008-4E3E-B1A9-018C3B175C02}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {B6227BCB-9008-4E3E-B1A9-018C3B175C02}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {98DBF905-A5BF-44F8-B69D-FD94A092F397}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {98DBF905-A5BF-44F8-B69D-FD94A092F397}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {98DBF905-A5BF-44F8-B69D-FD94A092F397}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {98DBF905-A5BF-44F8-B69D-FD94A092F397}.Release|Any CPU.Build.0 = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(SolutionProperties) = preSolution 34 | HideSolutionNode = FALSE 35 | EndGlobalSection 36 | GlobalSection(ExtensibilityGlobals) = postSolution 37 | SolutionGuid = {61770E4B-4308-411E-A89C-458025473241} 38 | EndGlobalSection 39 | EndGlobal 40 | -------------------------------------------------------------------------------- /GChan/4chanIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Issung/GChan/941daf0fa9e104768c0d7b4b635c889d96532dd4/GChan/4chanIcon.ico -------------------------------------------------------------------------------- /GChan/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | C:\GChan 15 | 16 | 17 | 60000 18 | 19 | 20 | False 21 | 22 | 23 | True 24 | 25 | 26 | False 27 | 28 | 29 | True 30 | 31 | 32 | True 33 | 34 | 35 | True 36 | 37 | 38 | False 39 | 40 | 41 | 0 42 | 43 | 44 | 0 45 | 46 | 47 | False 48 | 49 | 50 | False 51 | 52 | 53 | True 54 | 55 | 56 | 35 57 | 58 | 59 | False 60 | 61 | 62 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /GChan/BoundListView.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace GChan 2 | { 3 | partial class BoundListView 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Component Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | components = new System.ComponentModel.Container(); 32 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 33 | } 34 | 35 | #endregion 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /GChan/BoundListView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Data; 4 | using System.Windows.Forms; 5 | 6 | namespace Binding 7 | { 8 | /// 9 | /// This class inherits from the ListView control 10 | /// and allows binding the control to a datasource 11 | /// 12 | public class BoundListView : ListView 13 | { 14 | #region --SYSTEM CODE-- 15 | 16 | private System.ComponentModel.Container components = null; 17 | 18 | #region Component Designer generated code 19 | 20 | private void InitializeComponent() 21 | { 22 | // 23 | // CompassListView 24 | // 25 | this.Name = "CompassListView"; 26 | } 27 | #endregion 28 | #endregion 29 | 30 | #region --VARIABLES-- 31 | CurrencyManager cm = null; 32 | DataView dv = null; 33 | #endregion 34 | 35 | #region --CONSTRUCTOR & DESTRUCTOR-- 36 | public BoundListView() 37 | { 38 | // This call is required by the Windows.Forms Form Designer. 39 | InitializeComponent(); 40 | 41 | base.SelectedIndexChanged +=new EventHandler(CompassListView_SelectedIndexChanged); 42 | base.ColumnClick +=new ColumnClickEventHandler(CompassListView_ColumnClick); 43 | } 44 | 45 | protected override void Dispose( bool disposing ) 46 | { 47 | if( disposing ) 48 | { 49 | if(components != null) 50 | { 51 | components.Dispose(); 52 | } 53 | } 54 | base.Dispose( disposing ); 55 | } 56 | #endregion 57 | 58 | #region --PROPERTIES-- 59 | #region --DATASOURCE-- 60 | private Object source; 61 | 62 | [Bindable(true)] 63 | [TypeConverter("System.Windows.Forms.Design.DataSourceConverter, System.Design")] 64 | [Category("Data")] 65 | [DefaultValue(null)] 66 | public Object DataSource 67 | { 68 | get 69 | { 70 | return source; 71 | } 72 | set 73 | { 74 | source = value; 75 | } 76 | } 77 | #endregion 78 | 79 | #region --DATAMEMBER-- 80 | 81 | private String data; 82 | 83 | [Category("Data"), Bindable(false)] 84 | [Editor("System.Windows.Forms.Design.DataMemberFieldEditor, System.Design", "System.Drawing.Design.UITypeEditor, System.Drawing")] 85 | [RefreshProperties(RefreshProperties.All)] 86 | [DefaultValue("")] 87 | public String DataMember 88 | { 89 | get 90 | { 91 | return data; 92 | } 93 | set 94 | { 95 | data = value; 96 | bind(); 97 | } 98 | } 99 | 100 | #endregion 101 | 102 | #region --SORTING-- 103 | 104 | [Browsable(false)] 105 | public new SortOrder Sorting 106 | { 107 | get 108 | { 109 | return base.Sorting; 110 | } 111 | set 112 | { 113 | base.Sorting = value; 114 | } 115 | } 116 | #endregion 117 | 118 | [Browsable(false)] 119 | protected new bool MultiSelect 120 | { 121 | get 122 | { 123 | return base.MultiSelect; 124 | } 125 | set 126 | { 127 | base.MultiSelect = false; 128 | } 129 | } 130 | #endregion 131 | 132 | #region --METHODS-- 133 | /// 134 | /// This method is called everytime the DataMember property is set 135 | /// 136 | private void bind() 137 | { 138 | Items.Clear(); //clear the existing list 139 | 140 | if(source is DataSet) 141 | { 142 | DataSet ds = source as DataSet; 143 | DataTable dt = ds.Tables[0]; 144 | 145 | if(dt!=null) 146 | { 147 | cm = (CurrencyManager)BindingContext[ds, ds.Tables[0].TableName]; 148 | cm.CurrentChanged +=new EventHandler(cm_CurrentChanged); 149 | dv = (DataView)cm.List; 150 | 151 | Columns.Add(DataMember, ClientRectangle.Width - 17, HorizontalAlignment.Left); 152 | 153 | foreach(DataRow dr in dt.Rows) 154 | { 155 | ListViewItem lvi = new ListViewItem(dr[DataMember].ToString()); 156 | lvi.Tag = dr; 157 | Items.Add(lvi); 158 | } 159 | Sorting = SortOrder.Ascending; 160 | dv.Sort = this.Columns[0].Text + " ASC"; 161 | } 162 | } 163 | else 164 | cm = null; 165 | } 166 | 167 | #endregion 168 | 169 | #region --EVENTS-- 170 | 171 | private void CompassListView_SelectedIndexChanged(object sender, EventArgs e) 172 | { 173 | if(this.SelectedIndices.Count>0) 174 | { 175 | if(cm!=null) 176 | { 177 | cm.Position = base.SelectedIndices[0]; 178 | } 179 | } 180 | } 181 | 182 | private void CompassListView_ColumnClick(object sender, ColumnClickEventArgs e) 183 | { 184 | if(Sorting==SortOrder.None || Sorting == SortOrder.Descending) 185 | { 186 | Sorting = SortOrder.Ascending; 187 | dv.Sort = this.Columns[0].Text + " ASC"; 188 | } 189 | else if(Sorting==SortOrder.Ascending) 190 | { 191 | Sorting = SortOrder.Descending; 192 | dv.Sort = this.Columns[0].Text + " DESC"; 193 | } 194 | } 195 | 196 | private void cm_CurrentChanged(object sender, EventArgs e) 197 | { 198 | this.Items[cm.Position].Selected = true; 199 | } 200 | 201 | #endregion 202 | } 203 | } -------------------------------------------------------------------------------- /GChan/Controllers/Download.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace GChan.Controllers 5 | { 6 | internal class Download : IDisposable 7 | { 8 | private readonly Thread thread; 9 | private readonly CancellationTokenSource cancellationTokenSource; 10 | 11 | public Download(Thread thread, CancellationTokenSource cancellationTokenSource) 12 | { 13 | this.thread = thread; 14 | this.cancellationTokenSource = cancellationTokenSource; 15 | } 16 | 17 | /// 18 | /// Signal cancellation to the thread. 19 | /// 20 | public void Cancel() 21 | { 22 | cancellationTokenSource.Cancel(); 23 | } 24 | 25 | /// 26 | /// Cancel download and dispose resources. 27 | /// 28 | public void Dispose() 29 | { 30 | this.cancellationTokenSource.Cancel(); 31 | this.cancellationTokenSource.Dispose(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /GChan/Controllers/DownloadManager.cs: -------------------------------------------------------------------------------- 1 | using NLog; 2 | using System; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | 8 | namespace GChan.Controllers 9 | { 10 | /// 11 | /// Class that manages a file download pool.
12 | /// must provide a good implementation of .
13 | ///
14 | /// 15 | /// TODO: Add ability to clear the manager completely, for certain situations e.g. someone disables the setting to save thread html.
16 | /// TODO: Use async/tasks instead of threads.
17 | ///
18 | public class DownloadManager : IDisposable where T: IDownloadable 19 | { 20 | public delegate void SuccessCallback(T item); 21 | public delegate void FailureCallback(T item, bool retry); 22 | 23 | /// 24 | /// How many operations may be running concurrently. 25 | /// 26 | public int ConcurrentCount { get; set; } = Properties.Settings.Default.MaximumConcurrentDownloads; 27 | 28 | private static readonly Logger logger = LogManager.GetCurrentClassLogger(); 29 | private static readonly TimeSpan interval = TimeSpan.FromSeconds(1); 30 | 31 | private readonly ConcurrentDictionary downloading = new(); 32 | private readonly ConcurrentQueue waiting = new(); 33 | private readonly bool removeSuccessfulItems; 34 | private readonly Timer timer; 35 | private readonly string typeName; 36 | 37 | /// 38 | /// Constructor. 39 | /// 40 | /// 41 | /// Should items that successfully download be removed from the download manager?
42 | /// If false, after a successful download will enter the back of the queue again, for later re-downloading. 43 | /// 44 | public DownloadManager(bool removeSuccessfulItems) 45 | { 46 | this.removeSuccessfulItems = removeSuccessfulItems; 47 | this.timer = new(TimerTick, null, TimeSpan.Zero, interval); 48 | this.typeName = typeof(T).Name + "s"; 49 | } 50 | 51 | public void Queue(T item) 52 | { 53 | // Don't queue if already downloading or already in waiting list. 54 | if (!downloading.ContainsKey(item)) 55 | { 56 | if (!waiting.Contains(item)) // TODO: What is the performance of this? 57 | { 58 | logger.Trace("Queueing {item} for download.", item); 59 | waiting.Enqueue(item); 60 | return; 61 | } 62 | } 63 | } 64 | 65 | public void Queue(IEnumerable items) 66 | { 67 | foreach (var item in items) 68 | { 69 | Queue(item); 70 | } 71 | } 72 | 73 | /// 74 | /// Cancel a download of an item if it is currently downloading.
75 | /// To cancel downloads of items that are queued for download, set to false. 76 | ///
77 | public void Cancel(T item) 78 | { 79 | if (downloading.TryRemove(item, out var download)) 80 | { 81 | download.Dispose(); 82 | } 83 | } 84 | 85 | /// 86 | /// Cancel all currently downloading items that match .
87 | /// To cancel downloads of items that are queued for download, set to false. 88 | ///
89 | /// 90 | /// TODO: Not thread-safe because the dictionary could change over the loop. 91 | /// 92 | public void Cancel(Func predicate) 93 | { 94 | foreach (var kvp in downloading) 95 | { 96 | var item = kvp.Key; 97 | var download = kvp.Value; 98 | 99 | if (predicate(item)) 100 | { 101 | downloading.TryRemove(kvp.Key, out var _); 102 | download.Cancel(); 103 | } 104 | } 105 | } 106 | 107 | /// 108 | /// Clear the entire download manager (waiting and currently downloading), and cancel those that were in progress. 109 | /// 110 | public void Clear() 111 | { 112 | while (waiting.Count > 0) 113 | { 114 | waiting.TryDequeue(out _); 115 | } 116 | 117 | while (downloading.Count > 0) 118 | { 119 | var kvp = downloading.FirstOrDefault(); 120 | downloading.TryRemove(kvp.Key, out _); 121 | kvp.Value.Dispose(); 122 | } 123 | } 124 | 125 | /// 126 | /// A tick of the timer. 127 | /// 128 | /// Unnecessary paramater. 129 | private void TimerTick(object _) 130 | { 131 | var dequeueCount = ConcurrentCount - downloading.Count; 132 | var items = Dequeue(dequeueCount); 133 | logger.Trace("Dequeue {dequeue_count} {type} from queue, got {dequeue_result_count}.", dequeueCount, typeName, items.Count); // TODO: Appears to be double-logging. 134 | 135 | // TODO: If no images were found in queue set the timer to a slightly longer interval, to stop poll spamming. 136 | 137 | foreach (var item in items) 138 | { 139 | var cancellationTokenSource = new CancellationTokenSource(); 140 | var thread = new Thread(() => item.Download(DownloadSuccess, DownloadFailed, cancellationTokenSource.Token)); 141 | 142 | var download = new Download(thread, cancellationTokenSource); 143 | thread.Start(); 144 | 145 | downloading.TryAdd(item, download); 146 | } 147 | } 148 | 149 | /// 150 | /// Take some items off of the queue. 151 | /// 152 | private List Dequeue(int amount) 153 | { 154 | var chunk = new List(amount); 155 | 156 | while (chunk.Count < amount && waiting.TryDequeue(out var item)) 157 | { 158 | if (item.ShouldDownload) 159 | { 160 | chunk.Add(item); 161 | } 162 | else 163 | { 164 | // If shouldn't download don't add to chunk. The item has already been removed from the queue. 165 | } 166 | } 167 | 168 | return chunk; 169 | } 170 | 171 | /// 172 | /// Called when a download has completed successfully.
173 | /// Removes from the downloading dict. 174 | ///
175 | private void DownloadSuccess(T item) 176 | { 177 | logger.Trace("Item {item} completed downloading succesfully.", item); 178 | 179 | if (downloading.TryRemove(item, out var _)) 180 | { 181 | // If manager is not supposed remove items after a successful download, add back onto the queue. 182 | if (!removeSuccessfulItems) 183 | { 184 | waiting.Enqueue(item); 185 | } 186 | } 187 | else 188 | { 189 | logger.Warn("DownloadSuccess callback was called with {item} but was not in the downloading dictionary.", item); 190 | } 191 | } 192 | 193 | /// 194 | /// Called when a download was unable to complete.
195 | /// Removes from the downloading dict and requeues it pending download.
196 | /// If the failure is permanent (e.g. image is gone) then can be set to false. 197 | ///
198 | private void DownloadFailed(T item, bool retry) 199 | { 200 | logger.Trace("Item {item} downloading failed.", item); 201 | 202 | if (downloading.TryRemove(item, out var _)) 203 | { 204 | if (retry) 205 | { 206 | waiting.Enqueue(item); 207 | } 208 | } 209 | else 210 | { 211 | logger.Warn("DownloadFailed callback was called with {item} but was not in the downloading dictionary.", item); 212 | } 213 | } 214 | 215 | public void Dispose() 216 | { 217 | timer.Dispose(); 218 | 219 | foreach (var kvp in downloading) 220 | { 221 | var download = kvp.Value; 222 | download.Cancel(); 223 | download.Dispose(); 224 | } 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /GChan/Controllers/UpdateController.cs: -------------------------------------------------------------------------------- 1 | using GChan.Data; 2 | using GChan.Forms; 3 | using Onova; 4 | using Onova.Models; 5 | using Onova.Services; 6 | using System; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Threading.Tasks; 10 | using System.Windows.Forms; 11 | 12 | namespace GChan.Controllers 13 | { 14 | /// 15 | /// Controller that manages the check for updates/updating version flow. 16 | /// 17 | class UpdateController 18 | { 19 | private static UpdateController instance = null; 20 | 21 | public static UpdateController Instance 22 | { 23 | get 24 | { 25 | instance ??= new UpdateController(); 26 | return instance; 27 | } 28 | } 29 | 30 | readonly IUpdateManager updateManager = new UpdateManager( 31 | new GithubPackageResolver(Program.GITHUB_REPOSITORY_OWNER, Program.GITHUB_REPOSITORY_NAME, $"{Program.NAME}-*.zip"), 32 | new ZipPackageExtractor() 33 | ); 34 | 35 | /// 36 | /// Progress reporter for update downloading process. 37 | /// 38 | public Progress Progress = new Progress(); 39 | 40 | public CheckForUpdatesResult CheckForUpdatesResult { get; private set; } 41 | 42 | // Update Check Started 43 | public delegate void UpdateCheckStartedEvent(object sender, bool initiatedByUser); 44 | public event UpdateCheckStartedEvent UpdateCheckStarted; 45 | 46 | // Update Check Finished 47 | public delegate void UpdateCheckFinishedEvent(object sender, CheckForUpdatesResult result, bool initiatedByUser); 48 | public event UpdateCheckFinishedEvent UpdateCheckFinished; 49 | 50 | // Update Started 51 | public delegate void UpdateStartedEvent(object sender); 52 | public event UpdateStartedEvent UpdateStarted; 53 | 54 | public string CurrentVersion { get; private set; } 55 | 56 | UpdateInfoForm infoForm; 57 | 58 | private UpdateController() 59 | { 60 | CurrentVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString().Trim(); 61 | 62 | // Save current version and cut off trailing ".0"'s. 63 | while (string.Join("", CurrentVersion.Skip(Math.Max(0, CurrentVersion.Length - 2))) == ".0") 64 | { 65 | CurrentVersion = CurrentVersion.Substring(0, CurrentVersion.Length - 2); 66 | } 67 | } 68 | 69 | public async void CheckForUpdates(bool initiatedByUser) 70 | { 71 | UpdateCheckStarted?.Invoke(this, initiatedByUser); 72 | 73 | CheckForUpdatesResult = await updateManager.CheckForUpdatesAsync(); 74 | 75 | UpdateCheckFinished?.Invoke(this, CheckForUpdatesResult, initiatedByUser); 76 | } 77 | 78 | public void ShowUpdateDialog() 79 | { 80 | infoForm = new UpdateInfoForm 81 | { 82 | TopMost = true 83 | }; 84 | infoForm.ShowDialog(); 85 | } 86 | 87 | public async Task PerformUpdate() 88 | { 89 | if (CheckForUpdatesResult.CanUpdate) 90 | { 91 | UpdateStarted?.Invoke(this); 92 | 93 | // Prepare the latest update 94 | await updateManager.PrepareUpdateAsync(CheckForUpdatesResult.LastVersion, Progress); 95 | 96 | // Launch updater and exit 97 | updateManager.LaunchUpdater(CheckForUpdatesResult.LastVersion); 98 | 99 | infoForm?.Close(); 100 | 101 | Program.mainForm.FormClosing -= Program.mainForm.MainForm_FormClosing; 102 | 103 | DataController.SaveAll(Program.mainForm.Model.Threads, Program.mainForm.Model.Boards); 104 | 105 | Application.Exit(); 106 | } 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /GChan/Controls/DataGridViewPreferencesSetting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | 5 | /// 6 | /// Credit goes to Günther M. FOIDL https://github.com/gfoidl 7 | /// https://www.codeproject.com/Articles/37087/DataGridView-that-Saves-Column-Order-Width-and-Vis 8 | /// 9 | namespace GChan.Controls 10 | { 11 | internal sealed class DataGridViewPreferencesSetting : ApplicationSettingsBase 12 | { 13 | private static DataGridViewPreferencesSetting _defaultInstance = (DataGridViewPreferencesSetting)Synchronized(new DataGridViewPreferencesSetting()); 14 | 15 | public static DataGridViewPreferencesSetting Default => _defaultInstance; 16 | 17 | // Because there can be more than one DGV in the user-application 18 | // a dictionary is used to save the settings for this DGV. 19 | // As name of the control is used as the dictionary key. 20 | [UserScopedSetting] 21 | [SettingsSerializeAs(SettingsSerializeAs.Binary)] 22 | [DefaultSettingValue("")] 23 | public Dictionary> ColumnOrder 24 | { 25 | get { return this["ColumnOrder"] as Dictionary>; } 26 | set { this["ColumnOrder"] = value; } 27 | } 28 | 29 | 30 | [UserScopedSetting] 31 | [SettingsSerializeAs(SettingsSerializeAs.String)] 32 | [DefaultSettingValue("True")] 33 | public bool FirstStart 34 | { 35 | get 36 | { 37 | return (bool)(this[nameof(FirstStart)]); 38 | } 39 | set 40 | { 41 | this[nameof(FirstStart)] = value; 42 | } 43 | } 44 | } 45 | 46 | [Serializable] 47 | public sealed class ColumnOrderItem 48 | { 49 | public int DisplayIndex { get; set; } 50 | public float FillWeight { get; set; } 51 | public bool Visible { get; set; } 52 | public int ColumnIndex { get; set; } 53 | } 54 | } -------------------------------------------------------------------------------- /GChan/Controls/PreferencesDataGridView.cs: -------------------------------------------------------------------------------- 1 | using NLog; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Drawing; 6 | using System.Linq; 7 | using System.Windows.Forms; 8 | 9 | /// 10 | /// Credit goes to Günther M. FOIDL https://github.com/gfoidl 11 | /// https://www.codeproject.com/Articles/37087/DataGridView-that-Saves-Column-Order-Width-and-Vis 12 | /// 13 | namespace GChan.Controls 14 | { 15 | [Description("Enhanced DataGridView with inbuilt column hide/show context menu and saving/loading preferences of column's visibility, width & ordering.")] 16 | [ToolboxBitmap(typeof(DataGridView))] 17 | class PreferencesDataGridView : DataGridView 18 | { 19 | private readonly ContextMenuStrip contextMenu = new ContextMenuStrip(); 20 | private readonly ILogger logger = LogManager.GetCurrentClassLogger(); 21 | 22 | protected override bool ShowFocusCues => false; 23 | 24 | protected override void OnCreateControl() 25 | { 26 | base.OnCreateControl(); 27 | 28 | if (!DesignMode) 29 | { 30 | try 31 | { 32 | LoadPreferences(); 33 | } 34 | catch (Exception ex) 35 | { 36 | logger.Error(ex, "Failed to load datagridview preferences."); 37 | } 38 | 39 | foreach (DataGridViewColumn column in Columns) 40 | { 41 | ToolStripMenuItem toolStripItem = new ToolStripMenuItem(column.HeaderText) 42 | { 43 | CheckOnClick = true, 44 | Checked = column.Visible, 45 | }; 46 | toolStripItem.CheckStateChanged += ToolStripItem_CheckStateChanged; 47 | contextMenu.Items.Add(toolStripItem); 48 | column.HeaderCell.ContextMenuStrip = contextMenu; 49 | } 50 | } 51 | } 52 | 53 | private void ToolStripItem_CheckStateChanged(object sender, EventArgs e) 54 | { 55 | var item = (ToolStripMenuItem)sender; 56 | Columns[contextMenu.Items.IndexOf(item)].Visible = item.Checked; 57 | } 58 | 59 | private void LoadPreferences() 60 | { 61 | // Load settings from last version if this is first run. 62 | if (DataGridViewPreferencesSetting.Default.FirstStart) 63 | { 64 | DataGridViewPreferencesSetting.Default.Upgrade(); 65 | DataGridViewPreferencesSetting.Default.Reload(); 66 | DataGridViewPreferencesSetting.Default.FirstStart = false; 67 | DataGridViewPreferencesSetting.Default.Save(); 68 | } 69 | 70 | if (!DataGridViewPreferencesSetting.Default.ColumnOrder.ContainsKey(this.Name)) 71 | { 72 | return; 73 | } 74 | 75 | var columnOrder = DataGridViewPreferencesSetting.Default.ColumnOrder[this.Name]; 76 | 77 | if (columnOrder != null) 78 | { 79 | var sorted = columnOrder.OrderBy(i => i.DisplayIndex); 80 | foreach (var item in sorted) 81 | { 82 | Columns[item.ColumnIndex].DisplayIndex = item.DisplayIndex; 83 | Columns[item.ColumnIndex].Visible = item.Visible; 84 | Columns[item.ColumnIndex].FillWeight = item.FillWeight; 85 | } 86 | } 87 | } 88 | 89 | protected override void Dispose(bool disposing) 90 | { 91 | SavePreferences(); 92 | base.Dispose(disposing); 93 | } 94 | 95 | private void SavePreferences() 96 | { 97 | List columnOrder = new List(); 98 | DataGridViewColumnCollection columns = Columns; 99 | 100 | for (int i = 0; i < columns.Count; i++) 101 | { 102 | columnOrder.Add(new ColumnOrderItem 103 | { 104 | ColumnIndex = i, 105 | DisplayIndex = columns[i].DisplayIndex, 106 | Visible = columns[i].Visible, 107 | FillWeight = columns[i].FillWeight 108 | }); 109 | } 110 | 111 | DataGridViewPreferencesSetting.Default.ColumnOrder[this.Name] = columnOrder; 112 | DataGridViewPreferencesSetting.Default.FirstStart = false; 113 | DataGridViewPreferencesSetting.Default.Save(); 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /GChan/Controls/SortableBindingList/PropertyComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Reflection; 6 | 7 | namespace GChan.Controls 8 | { 9 | public class PropertyComparer : IComparer 10 | { 11 | private readonly IComparer comparer; 12 | private PropertyDescriptor propertyDescriptor; 13 | private int reverse; 14 | 15 | public PropertyComparer(PropertyDescriptor property, ListSortDirection direction) 16 | { 17 | propertyDescriptor = property; 18 | Type comparerForPropertyType = typeof(Comparer<>).MakeGenericType(property.PropertyType); 19 | comparer = (IComparer)comparerForPropertyType.InvokeMember("Default", BindingFlags.Static | BindingFlags.GetProperty | BindingFlags.Public, null, null, null); 20 | SetListSortDirection(direction); 21 | } 22 | 23 | #region IComparer Members 24 | 25 | public int Compare(T x, T y) 26 | { 27 | return reverse * comparer.Compare(propertyDescriptor.GetValue(x), propertyDescriptor.GetValue(y)); 28 | } 29 | 30 | #endregion 31 | 32 | private void SetPropertyDescriptor(PropertyDescriptor descriptor) 33 | { 34 | propertyDescriptor = descriptor; 35 | } 36 | 37 | private void SetListSortDirection(ListSortDirection direction) 38 | { 39 | reverse = direction == ListSortDirection.Ascending ? 1 : -1; 40 | } 41 | 42 | public void SetPropertyAndDirection(PropertyDescriptor descriptor, ListSortDirection direction) 43 | { 44 | SetPropertyDescriptor(descriptor); 45 | SetListSortDirection(direction); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /GChan/Controls/SortableBindingList/SortableBindingList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Drawing.Imaging; 5 | using System.Linq; 6 | 7 | namespace GChan.Controls 8 | { 9 | /// 10 | /// Binding list that gives list changed events, also supports sorting for the WinForms UI. 11 | /// Attempts to be thread-safe as possible within reason. 12 | /// 13 | public class SortableBindingList : BindingList 14 | { 15 | private readonly Dictionary> comparers; 16 | private bool isSorted; 17 | private ListSortDirection listSortDirection; 18 | private PropertyDescriptor propertyDescriptor; 19 | 20 | private readonly object ilock = new(); 21 | 22 | public SortableBindingList() 23 | : base(new List()) 24 | { 25 | comparers = new Dictionary>(); 26 | } 27 | 28 | public SortableBindingList(IEnumerable enumeration) 29 | : base(new List(enumeration)) 30 | { 31 | comparers = new Dictionary>(); 32 | } 33 | 34 | protected override bool SupportsSortingCore 35 | { 36 | get { return true; } 37 | } 38 | 39 | protected override bool IsSortedCore 40 | { 41 | get { return isSorted; } 42 | } 43 | 44 | protected override PropertyDescriptor SortPropertyCore 45 | { 46 | get { return propertyDescriptor; } 47 | } 48 | 49 | protected override ListSortDirection SortDirectionCore 50 | { 51 | get { return listSortDirection; } 52 | } 53 | 54 | protected override bool SupportsSearchingCore 55 | { 56 | get { return true; } 57 | } 58 | 59 | public new void Add(T item) 60 | { 61 | lock (ilock) 62 | { 63 | base.Add(item); 64 | } 65 | } 66 | 67 | public new T this[int index] 68 | { 69 | get 70 | { 71 | lock (ilock) 72 | { 73 | return base[index]; 74 | } 75 | } 76 | set 77 | { 78 | lock (ilock) 79 | { 80 | base[index] = value; 81 | } 82 | } 83 | } 84 | 85 | protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction) 86 | { 87 | List itemsList = (List)Items; 88 | 89 | Type propertyType = property.PropertyType; 90 | PropertyComparer comparer; 91 | if (!comparers.TryGetValue(propertyType, out comparer)) 92 | { 93 | comparer = new PropertyComparer(property, direction); 94 | comparers.Add(propertyType, comparer); 95 | } 96 | 97 | comparer.SetPropertyAndDirection(property, direction); 98 | itemsList.Sort(comparer); 99 | 100 | propertyDescriptor = property; 101 | listSortDirection = direction; 102 | isSorted = true; 103 | 104 | OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); 105 | } 106 | 107 | protected override void RemoveSortCore() 108 | { 109 | isSorted = false; 110 | propertyDescriptor = base.SortPropertyCore; 111 | listSortDirection = base.SortDirectionCore; 112 | 113 | OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); 114 | } 115 | 116 | protected override int FindCore(PropertyDescriptor property, object key) 117 | { 118 | int count = Count; 119 | for (int i = 0; i < count; ++i) 120 | { 121 | T element = this[i]; 122 | if (property.GetValue(element).Equals(key)) 123 | { 124 | return i; 125 | } 126 | } 127 | 128 | return -1; 129 | } 130 | 131 | /// Item removed from . 132 | public new T RemoveAt(int index) 133 | { 134 | lock (ilock) 135 | { 136 | var item = this[index]; 137 | base.RemoveAt(index); 138 | return item; 139 | } 140 | } 141 | 142 | public new void Remove(T item) 143 | { 144 | lock (ilock) 145 | { 146 | if (Contains(item)) 147 | { 148 | base.Remove(item); 149 | } 150 | } 151 | } 152 | 153 | /// Removed items. 154 | public T[] RemoveAll(Predicate match) 155 | { 156 | lock (ilock) 157 | { 158 | var itemsToRemove = new List(); 159 | 160 | foreach (T item in this) 161 | { 162 | if (match(item)) 163 | { 164 | itemsToRemove.Add(item); 165 | } 166 | } 167 | 168 | foreach (T item in itemsToRemove) 169 | { 170 | this.Remove(item); 171 | } 172 | 173 | return itemsToRemove.ToArray(); 174 | } 175 | } 176 | 177 | /// 178 | /// Lock the list and foreach loop over the items (don't do long operations with this). 179 | /// 180 | public void ForEachLocked(Action action) 181 | { 182 | lock (ilock) 183 | { 184 | foreach (var item in this) 185 | { 186 | action(item); 187 | } 188 | } 189 | } 190 | 191 | /// 192 | /// Thread Safe implementation. 193 | /// 194 | public List ToList() 195 | { 196 | lock (ilock) 197 | { 198 | return Enumerable.ToList(this); 199 | } 200 | } 201 | 202 | /// 203 | /// Thread Safe implementation. 204 | /// 205 | public T[] ToArray() 206 | { 207 | lock (ilock) 208 | { 209 | return Enumerable.ToArray(this); 210 | } 211 | } 212 | 213 | public T[] Copy() => ToArray(); 214 | } 215 | } -------------------------------------------------------------------------------- /GChan/Data/DataController.cs: -------------------------------------------------------------------------------- 1 | using GChan.Trackers; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Data.SQLite; 5 | using System.IO; 6 | 7 | namespace GChan.Data 8 | { 9 | class DataController 10 | { 11 | /// 12 | /// Current database version, change this when database structure is changed. 13 | /// 14 | public const int DATABASE_VERSION = 2; 15 | 16 | private const string DATABASE_FILENAME = "trackers.db"; 17 | private static readonly string DATABASE_FOLDERNAME = "data"; 18 | private static readonly string DATABASE_PATH = Path.Combine(AppContext.BaseDirectory, DATABASE_FOLDERNAME, DATABASE_FILENAME); 19 | private static readonly string DATABASE_CONNECTION_STRING = "DataSource=" + DATABASE_PATH; 20 | 21 | /// 22 | /// Tables 23 | /// 24 | private const string TB_VERSION = "version"; 25 | private const string TB_THREAD = "thread"; 26 | private const string TB_BOARD = "board"; 27 | 28 | // Columns 29 | private const string COL_VERSION = "version"; 30 | private const string COL_URL = "url"; 31 | 32 | // Thread columns 33 | private const string COL_SUBJECT = "subject"; 34 | private const string COL_SAVED_IDS = "saved_ids"; 35 | 36 | // Board columns 37 | private const string COL_GREATEST_THREAD_ID = "greatest_thread_id"; 38 | 39 | static SQLiteConnection _connection = null; 40 | 41 | static object _connectionLock = new object(); 42 | 43 | static SQLiteConnection Connection 44 | { 45 | get 46 | { 47 | lock (_connectionLock) 48 | { 49 | bool dbExisted = File.Exists(DATABASE_PATH); 50 | 51 | if (!dbExisted) 52 | { 53 | Directory.CreateDirectory(DATABASE_FOLDERNAME); 54 | SQLiteConnection.CreateFile(DATABASE_PATH); 55 | } 56 | 57 | if (_connection == null) 58 | { 59 | _connection = new SQLiteConnection(DATABASE_CONNECTION_STRING); 60 | _connection.Open(); 61 | 62 | if (!dbExisted) 63 | { 64 | CreateDB(); 65 | } 66 | else 67 | { 68 | UpgradeDB(); 69 | } 70 | } 71 | 72 | return _connection; 73 | } 74 | } 75 | } 76 | 77 | private static void CreateDB() 78 | { 79 | using (var cmd = new SQLiteCommand(Connection)) 80 | { 81 | // Version Table 82 | cmd.CommandText = $"DROP TABLE IF EXISTS {TB_VERSION}"; 83 | cmd.ExecuteNonQuery(); 84 | 85 | cmd.CommandText = $"CREATE TABLE {TB_VERSION} ({COL_VERSION} INTEGER PRIMARY KEY NOT NULL)"; 86 | cmd.ExecuteNonQuery(); 87 | 88 | cmd.CommandText = $"INSERT INTO {TB_VERSION} ({COL_VERSION}) VALUES (@{COL_VERSION})"; 89 | cmd.Parameters.AddWithValue(COL_VERSION, DATABASE_VERSION); 90 | cmd.ExecuteNonQuery(); 91 | 92 | // Thread Table 93 | cmd.CommandText = $"DROP TABLE IF EXISTS {TB_THREAD}"; 94 | cmd.ExecuteNonQuery(); 95 | 96 | cmd.CommandText = $"CREATE TABLE {TB_THREAD} ({COL_URL} TEXT PRIMARY KEY NOT NULL, {COL_SUBJECT} TEXT, {COL_SAVED_IDS} TEXT)"; 97 | cmd.ExecuteNonQuery(); 98 | 99 | // Board Table 100 | cmd.CommandText = $"DROP TABLE IF EXISTS {TB_BOARD}"; 101 | cmd.ExecuteNonQuery(); 102 | 103 | cmd.CommandText = $"CREATE TABLE {TB_BOARD} ({COL_URL} TEXT PRIMARY KEY NOT NULL, {COL_GREATEST_THREAD_ID} INTEGER)"; 104 | cmd.ExecuteNonQuery(); 105 | } 106 | } 107 | 108 | /// 109 | /// Drop the current database and build a new one. 110 | /// 111 | private static void UpgradeDB() 112 | { 113 | int version = -1; 114 | 115 | using (var cmd = new SQLiteCommand(Connection)) 116 | { 117 | cmd.CommandText = $"SELECT * FROM {TB_VERSION}"; 118 | 119 | using SQLiteDataReader reader = cmd.ExecuteReader(); 120 | 121 | if (reader.Read()) 122 | { 123 | version = reader.GetInt32(0); 124 | } 125 | } 126 | 127 | if (version == -1 || version != DATABASE_VERSION) 128 | { 129 | // TODO: Figure out migration strategies. 130 | CreateDB(); 131 | } 132 | } 133 | 134 | /// 135 | /// Saves the thread and board lists to disk. 136 | /// 137 | public static void SaveAll(IList threads, IList boards) 138 | { 139 | SaveThreads(threads); 140 | SaveBoards(boards); 141 | } 142 | 143 | public static void SaveThreads(IList threads) 144 | { 145 | using (var cmd = new SQLiteCommand(Connection)) 146 | { 147 | cmd.CommandText = $"DELETE FROM {TB_THREAD}"; 148 | 149 | cmd.ExecuteNonQuery(); 150 | 151 | for (int i = 0; i < threads.Count; i++) 152 | { 153 | cmd.CommandText = $@"INSERT INTO 154 | {TB_THREAD} ({COL_URL}, {COL_SUBJECT}, {COL_SAVED_IDS}) 155 | VALUES 156 | (@{COL_URL}, @{COL_SUBJECT}, @{COL_SAVED_IDS})"; 157 | 158 | cmd.Parameters.AddWithValue(COL_URL, threads[i].Url); 159 | cmd.Parameters.AddWithValue(COL_SUBJECT, threads[i].Subject); 160 | cmd.Parameters.AddWithValue(COL_SAVED_IDS, threads[i].SavedIds.ToStringList()); 161 | 162 | cmd.ExecuteNonQuery(); 163 | } 164 | } 165 | } 166 | 167 | public static IList LoadThreads() 168 | { 169 | var loadedThreads = new List(); 170 | 171 | using (var cmd = new SQLiteCommand(Connection)) 172 | { 173 | cmd.CommandText = $"SELECT * FROM {TB_THREAD}"; 174 | 175 | using SQLiteDataReader reader = cmd.ExecuteReader(); 176 | while (reader.Read()) 177 | { 178 | loadedThreads.Add(new LoadedThreadData(reader)); 179 | } 180 | } 181 | 182 | return loadedThreads; 183 | } 184 | 185 | public static void SaveBoards(IList boards) 186 | { 187 | using var cmd = new SQLiteCommand(Connection); 188 | cmd.CommandText = $"DELETE FROM {TB_BOARD}"; 189 | cmd.ExecuteNonQuery(); 190 | 191 | for (int i = 0; i < boards.Count; i++) 192 | { 193 | cmd.CommandText = $@"INSERT INTO {TB_BOARD} ({COL_URL}, {COL_GREATEST_THREAD_ID}) VALUES (@{COL_URL}, @{COL_GREATEST_THREAD_ID})"; 194 | cmd.Parameters.AddWithValue(COL_URL, boards[i].Url); 195 | cmd.Parameters.AddWithValue(COL_GREATEST_THREAD_ID, boards[i].GreatestThreadId); 196 | 197 | cmd.ExecuteNonQuery(); 198 | } 199 | } 200 | 201 | public static IList LoadBoards() 202 | { 203 | var loadedBoards = new List(); 204 | 205 | using (var cmd = new SQLiteCommand(Connection)) 206 | { 207 | cmd.CommandText = $"SELECT * FROM {TB_BOARD}"; 208 | 209 | using SQLiteDataReader reader = cmd.ExecuteReader(); 210 | while (reader.Read()) 211 | { 212 | loadedBoards.Add(new LoadedBoardData(reader)); 213 | } 214 | } 215 | 216 | return loadedBoards; 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /GChan/Data/LoadedBoardData.cs: -------------------------------------------------------------------------------- 1 | using System.Data.SQLite; 2 | 3 | namespace GChan.Data 4 | { 5 | public class LoadedBoardData : LoadedData 6 | { 7 | public long GreatestThreadId; 8 | 9 | public LoadedBoardData(SQLiteDataReader dataReader) 10 | { 11 | var index = 0; 12 | Url = dataReader.GetString(index++) ?? ""; 13 | GreatestThreadId = dataReader.GetInt64(index++); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /GChan/Data/LoadedData.cs: -------------------------------------------------------------------------------- 1 | namespace GChan.Data 2 | { 3 | /// 4 | /// Data loaded from database. 5 | /// 6 | public class LoadedData 7 | { 8 | /// 9 | /// Url of the tracker. 10 | /// 11 | public string Url; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /GChan/Data/LoadedThreadData.cs: -------------------------------------------------------------------------------- 1 | using GChan.Models; 2 | using System.Data.SQLite; 3 | 4 | namespace GChan.Data 5 | { 6 | public class LoadedThreadData : LoadedData 7 | { 8 | public string Subject; 9 | 10 | public SavedIdsCollection SavedIds; 11 | 12 | public LoadedThreadData(SQLiteDataReader dataReader) 13 | { 14 | var index = 0; 15 | Url = dataReader.GetString(index++) ?? ""; 16 | Subject = dataReader.GetString(index++) ?? ""; 17 | SavedIds = new SavedIdsCollection(dataReader.GetString(index++) ?? ""); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /GChan/Forms/AboutBox.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace GChan 2 | { 3 | partial class AboutBox 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | protected override void Dispose(bool disposing) 14 | { 15 | if (disposing && (components != null)) 16 | { 17 | components.Dispose(); 18 | } 19 | base.Dispose(disposing); 20 | } 21 | 22 | #region Windows Form Designer generated code 23 | 24 | /// 25 | /// Required method for Designer support - do not modify 26 | /// the contents of this method with the code editor. 27 | /// 28 | private void InitializeComponent() 29 | { 30 | System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(AboutBox)); 31 | this.okButton = new System.Windows.Forms.Button(); 32 | this.labelCompanyName = new System.Windows.Forms.Label(); 33 | this.labelCopyright = new System.Windows.Forms.Label(); 34 | this.labelVersion = new System.Windows.Forms.Label(); 35 | this.labelProductName = new System.Windows.Forms.Label(); 36 | this.tableLayoutPanel = new System.Windows.Forms.TableLayoutPanel(); 37 | this.richTextBoxDescription = new System.Windows.Forms.RichTextBox(); 38 | this.tableLayoutPanel.SuspendLayout(); 39 | this.SuspendLayout(); 40 | // 41 | // okButton 42 | // 43 | this.okButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); 44 | this.okButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; 45 | this.okButton.Location = new System.Drawing.Point(339, 239); 46 | this.okButton.Name = "okButton"; 47 | this.okButton.Size = new System.Drawing.Size(75, 23); 48 | this.okButton.TabIndex = 24; 49 | this.okButton.Text = "&OK"; 50 | // 51 | // labelCompanyName 52 | // 53 | this.labelCompanyName.Dock = System.Windows.Forms.DockStyle.Fill; 54 | this.labelCompanyName.Location = new System.Drawing.Point(6, 78); 55 | this.labelCompanyName.Margin = new System.Windows.Forms.Padding(6, 0, 3, 0); 56 | this.labelCompanyName.MaximumSize = new System.Drawing.Size(0, 17); 57 | this.labelCompanyName.Name = "labelCompanyName"; 58 | this.labelCompanyName.Size = new System.Drawing.Size(408, 17); 59 | this.labelCompanyName.TabIndex = 22; 60 | this.labelCompanyName.Text = "Company Name"; 61 | this.labelCompanyName.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; 62 | // 63 | // labelCopyright 64 | // 65 | this.labelCopyright.Dock = System.Windows.Forms.DockStyle.Fill; 66 | this.labelCopyright.Location = new System.Drawing.Point(6, 52); 67 | this.labelCopyright.Margin = new System.Windows.Forms.Padding(6, 0, 3, 0); 68 | this.labelCopyright.MaximumSize = new System.Drawing.Size(0, 17); 69 | this.labelCopyright.Name = "labelCopyright"; 70 | this.labelCopyright.Size = new System.Drawing.Size(408, 17); 71 | this.labelCopyright.TabIndex = 21; 72 | this.labelCopyright.Text = "Copyright"; 73 | this.labelCopyright.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; 74 | // 75 | // labelVersion 76 | // 77 | this.labelVersion.Dock = System.Windows.Forms.DockStyle.Fill; 78 | this.labelVersion.Location = new System.Drawing.Point(6, 26); 79 | this.labelVersion.Margin = new System.Windows.Forms.Padding(6, 0, 3, 0); 80 | this.labelVersion.MaximumSize = new System.Drawing.Size(0, 17); 81 | this.labelVersion.Name = "labelVersion"; 82 | this.labelVersion.Size = new System.Drawing.Size(408, 17); 83 | this.labelVersion.TabIndex = 0; 84 | this.labelVersion.Text = "Version"; 85 | this.labelVersion.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; 86 | // 87 | // labelProductName 88 | // 89 | this.labelProductName.Dock = System.Windows.Forms.DockStyle.Fill; 90 | this.labelProductName.Location = new System.Drawing.Point(6, 0); 91 | this.labelProductName.Margin = new System.Windows.Forms.Padding(6, 0, 3, 0); 92 | this.labelProductName.MaximumSize = new System.Drawing.Size(0, 17); 93 | this.labelProductName.Name = "labelProductName"; 94 | this.labelProductName.Size = new System.Drawing.Size(408, 17); 95 | this.labelProductName.TabIndex = 19; 96 | this.labelProductName.Text = "Product Name"; 97 | this.labelProductName.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; 98 | // 99 | // tableLayoutPanel 100 | // 101 | this.tableLayoutPanel.ColumnCount = 1; 102 | this.tableLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 67F)); 103 | this.tableLayoutPanel.Controls.Add(this.labelProductName, 0, 0); 104 | this.tableLayoutPanel.Controls.Add(this.labelVersion, 0, 1); 105 | this.tableLayoutPanel.Controls.Add(this.labelCopyright, 0, 2); 106 | this.tableLayoutPanel.Controls.Add(this.labelCompanyName, 0, 3); 107 | this.tableLayoutPanel.Controls.Add(this.okButton, 0, 5); 108 | this.tableLayoutPanel.Controls.Add(this.richTextBoxDescription, 0, 4); 109 | this.tableLayoutPanel.Dock = System.Windows.Forms.DockStyle.Fill; 110 | this.tableLayoutPanel.Location = new System.Drawing.Point(9, 9); 111 | this.tableLayoutPanel.Name = "tableLayoutPanel"; 112 | this.tableLayoutPanel.RowCount = 6; 113 | this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F)); 114 | this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F)); 115 | this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F)); 116 | this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F)); 117 | this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F)); 118 | this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F)); 119 | this.tableLayoutPanel.Size = new System.Drawing.Size(417, 265); 120 | this.tableLayoutPanel.TabIndex = 0; 121 | // 122 | // richTextBoxDescription 123 | // 124 | this.richTextBoxDescription.BackColor = System.Drawing.SystemColors.Control; 125 | this.richTextBoxDescription.Dock = System.Windows.Forms.DockStyle.Fill; 126 | this.richTextBoxDescription.Location = new System.Drawing.Point(3, 107); 127 | this.richTextBoxDescription.Name = "richTextBoxDescription"; 128 | this.richTextBoxDescription.ReadOnly = true; 129 | this.richTextBoxDescription.Size = new System.Drawing.Size(411, 126); 130 | this.richTextBoxDescription.TabIndex = 25; 131 | this.richTextBoxDescription.Text = resources.GetString("richTextBoxDescription.Text"); 132 | this.richTextBoxDescription.LinkClicked += new System.Windows.Forms.LinkClickedEventHandler(this.richTextBoxDescription_LinkClicked); 133 | this.richTextBoxDescription.TextChanged += new System.EventHandler(this.richTextBoxDescription_TextChanged); 134 | // 135 | // AboutBox 136 | // 137 | this.AcceptButton = this.okButton; 138 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 139 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 140 | this.ClientSize = new System.Drawing.Size(435, 283); 141 | this.Controls.Add(this.tableLayoutPanel); 142 | this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; 143 | this.MaximizeBox = false; 144 | this.MinimizeBox = false; 145 | this.Name = "AboutBox"; 146 | this.Padding = new System.Windows.Forms.Padding(9); 147 | this.ShowIcon = false; 148 | this.ShowInTaskbar = false; 149 | this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; 150 | this.Text = "AboutBox"; 151 | this.tableLayoutPanel.ResumeLayout(false); 152 | this.ResumeLayout(false); 153 | 154 | } 155 | 156 | #endregion 157 | 158 | private System.Windows.Forms.Button okButton; 159 | private System.Windows.Forms.Label labelCompanyName; 160 | private System.Windows.Forms.Label labelCopyright; 161 | private System.Windows.Forms.Label labelVersion; 162 | private System.Windows.Forms.Label labelProductName; 163 | private System.Windows.Forms.TableLayoutPanel tableLayoutPanel; 164 | private System.Windows.Forms.RichTextBox richTextBoxDescription; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /GChan/Forms/AboutBox.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Windows.Forms; 4 | 5 | namespace GChan 6 | { 7 | partial class AboutBox : Form 8 | { 9 | public AboutBox() 10 | { 11 | InitializeComponent(); 12 | Text = String.Format("About {0}", AssemblyTitle); 13 | labelProductName.Text = AssemblyProduct; 14 | labelVersion.Text = String.Format("Version {0}", AssemblyVersion); 15 | labelCopyright.Text = AssemblyCopyright; 16 | labelCompanyName.Text = AssemblyCompany; 17 | } 18 | 19 | #region Assembly Attribute Accessors 20 | 21 | public string AssemblyTitle 22 | { 23 | get 24 | { 25 | object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyTitleAttribute), false); 26 | 27 | if (attributes.Length > 0) 28 | { 29 | AssemblyTitleAttribute titleAttribute = (AssemblyTitleAttribute)attributes[0]; 30 | if (titleAttribute.Title != "") 31 | { 32 | return titleAttribute.Title; 33 | } 34 | } 35 | 36 | return System.IO.Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().CodeBase); 37 | } 38 | } 39 | 40 | public string AssemblyVersion 41 | { 42 | get 43 | { 44 | return Assembly.GetExecutingAssembly().GetName().Version.ToString(); 45 | } 46 | } 47 | 48 | public string AssemblyDescription 49 | { 50 | get 51 | { 52 | object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyDescriptionAttribute), false); 53 | 54 | if (attributes.Length == 0) 55 | { 56 | return ""; 57 | } 58 | 59 | return ((AssemblyDescriptionAttribute)attributes[0]).Description; 60 | } 61 | } 62 | 63 | public string AssemblyProduct 64 | { 65 | get 66 | { 67 | object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyProductAttribute), false); 68 | 69 | if (attributes.Length == 0) 70 | { 71 | return ""; 72 | } 73 | 74 | return ((AssemblyProductAttribute)attributes[0]).Product; 75 | } 76 | } 77 | 78 | public string AssemblyCopyright 79 | { 80 | get 81 | { 82 | object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyCopyrightAttribute), false); 83 | 84 | if (attributes.Length == 0) 85 | { 86 | return ""; 87 | } 88 | 89 | return ((AssemblyCopyrightAttribute)attributes[0]).Copyright; 90 | } 91 | } 92 | 93 | public string AssemblyCompany 94 | { 95 | get 96 | { 97 | object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyCompanyAttribute), false); 98 | 99 | if (attributes.Length == 0) 100 | { 101 | return ""; 102 | } 103 | 104 | return ((AssemblyCompanyAttribute)attributes[0]).Company; 105 | } 106 | } 107 | 108 | #endregion Assembly Attribute Accessors 109 | 110 | private void richTextBoxDescription_LinkClicked(object sender, LinkClickedEventArgs e) 111 | { 112 | System.Diagnostics.Process.Start(e.LinkText); 113 | } 114 | 115 | private void richTextBoxDescription_TextChanged(object sender, EventArgs e) 116 | { 117 | 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /GChan/Forms/AboutBox.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | YChan forked and became GChan by Issung at 122 | https://github.com/Issung/GChan 123 | 124 | List of contributers to GChan (Thank you!) 125 | - MistressAshai 126 | - RoyalJackal 127 | 128 | YChan updated by Ricardo1991 and FugiMuffi at 129 | https://github.com/Ricardo1991/YChan 130 | https://github.com/FugiMuffi/YChan 131 | 132 | Original project (YChan): https://sourceforge.net/projects/ychan/ 133 | Created by mirage (mirage@secure-mail.biz) 134 | 135 | -------------------------------------------------------------------------------- /GChan/Forms/Changelog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Forms; 3 | 4 | namespace GChan 5 | { 6 | public partial class Changelog : Form 7 | { 8 | public Changelog() 9 | { 10 | InitializeComponent(); 11 | } 12 | 13 | private void closeButton_Click(object sender, EventArgs e) 14 | { 15 | Close(); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /GChan/Forms/Changelog.designer.cs: -------------------------------------------------------------------------------- 1 | namespace GChan { 2 | partial class Changelog { 3 | /// 4 | /// Required designer variable. 5 | /// 6 | private System.ComponentModel.IContainer components = null; 7 | 8 | /// 9 | /// Clean up any resources being used. 10 | /// 11 | /// true if managed resources should be disposed; otherwise, false. 12 | protected override void Dispose(bool disposing) { 13 | if(disposing && (components != null)) { 14 | components.Dispose(); 15 | } 16 | base.Dispose(disposing); 17 | } 18 | 19 | #region Windows Form Designer generated code 20 | 21 | /// 22 | /// Required method for Designer support - do not modify 23 | /// the contents of this method with the code editor. 24 | /// 25 | private void InitializeComponent() { 26 | System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Changelog)); 27 | this.rtbChange = new System.Windows.Forms.RichTextBox(); 28 | this.closeButton = new System.Windows.Forms.Button(); 29 | this.SuspendLayout(); 30 | // 31 | // rtbChange 32 | // 33 | this.rtbChange.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 34 | | System.Windows.Forms.AnchorStyles.Left) 35 | | System.Windows.Forms.AnchorStyles.Right))); 36 | this.rtbChange.BorderStyle = System.Windows.Forms.BorderStyle.None; 37 | this.rtbChange.Cursor = System.Windows.Forms.Cursors.Arrow; 38 | this.rtbChange.Location = new System.Drawing.Point(12, 12); 39 | this.rtbChange.Name = "rtbChange"; 40 | this.rtbChange.ReadOnly = true; 41 | this.rtbChange.Size = new System.Drawing.Size(486, 212); 42 | this.rtbChange.TabIndex = 0; 43 | this.rtbChange.Text = resources.GetString("rtbChange.Text"); 44 | // 45 | // closeButton 46 | // 47 | this.closeButton.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) 48 | | System.Windows.Forms.AnchorStyles.Right))); 49 | this.closeButton.Location = new System.Drawing.Point(12, 230); 50 | this.closeButton.Name = "closeButton"; 51 | this.closeButton.Size = new System.Drawing.Size(486, 23); 52 | this.closeButton.TabIndex = 1; 53 | this.closeButton.Text = "Close"; 54 | this.closeButton.UseVisualStyleBackColor = true; 55 | this.closeButton.Click += new System.EventHandler(this.closeButton_Click); 56 | // 57 | // Changelog 58 | // 59 | this.AcceptButton = this.closeButton; 60 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 61 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 62 | this.ClientSize = new System.Drawing.Size(510, 260); 63 | this.ControlBox = false; 64 | this.Controls.Add(this.closeButton); 65 | this.Controls.Add(this.rtbChange); 66 | this.Name = "Changelog"; 67 | this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; 68 | this.Text = "Changelog"; 69 | this.ResumeLayout(false); 70 | 71 | } 72 | 73 | #endregion 74 | 75 | private System.Windows.Forms.RichTextBox rtbChange; 76 | private System.Windows.Forms.Button closeButton; 77 | } 78 | } -------------------------------------------------------------------------------- /GChan/Forms/CloseWarn.cs: -------------------------------------------------------------------------------- 1 | using GChan.Properties; 2 | using System; 3 | using System.Windows.Forms; 4 | 5 | namespace GChan 6 | { 7 | public partial class CloseWarn : Form 8 | { 9 | private Label lblText; 10 | private Button btnClose; 11 | private Button btnNoClose; 12 | private CheckBox chkWarning; 13 | private CheckBox chkSave; 14 | 15 | public CloseWarn() 16 | { 17 | InitializeComponent(); 18 | } 19 | 20 | private void btnClose_Click(object sender, EventArgs e) 21 | { 22 | Close(); 23 | } 24 | 25 | private void btnNoClose_Click(object sender, EventArgs e) 26 | { 27 | Close(); 28 | } 29 | 30 | private void chkWarning_CheckedChanged(object sender, EventArgs e) 31 | { 32 | Settings.Default.WarnOnClose = !chkWarning.Checked; 33 | Settings.Default.Save(); 34 | } 35 | 36 | private void chkSave_CheckedChanged(object sender, EventArgs e) 37 | { 38 | Settings.Default.SaveListsOnClose = chkSave.Checked; 39 | Settings.Default.Save(); 40 | } 41 | 42 | private void CloseWarn_Load(object sender, EventArgs e) 43 | { 44 | chkSave.Checked = Settings.Default.SaveListsOnClose; 45 | chkWarning.Checked = !Settings.Default.WarnOnClose; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /GChan/Forms/CloseWarn.designer.cs: -------------------------------------------------------------------------------- 1 | namespace GChan 2 | { 3 | partial class CloseWarn 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | 30 | private void InitializeComponent() 31 | { 32 | System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(CloseWarn)); 33 | this.lblText = new System.Windows.Forms.Label(); 34 | this.btnClose = new System.Windows.Forms.Button(); 35 | this.btnNoClose = new System.Windows.Forms.Button(); 36 | this.chkWarning = new System.Windows.Forms.CheckBox(); 37 | this.chkSave = new System.Windows.Forms.CheckBox(); 38 | this.buttonTableLayoutPanel = new System.Windows.Forms.TableLayoutPanel(); 39 | this.buttonTableLayoutPanel.SuspendLayout(); 40 | this.SuspendLayout(); 41 | // 42 | // lblText 43 | // 44 | this.lblText.AutoSize = true; 45 | this.lblText.Location = new System.Drawing.Point(12, 11); 46 | this.lblText.Name = "lblText"; 47 | this.lblText.Size = new System.Drawing.Size(406, 13); 48 | this.lblText.TabIndex = 0; 49 | this.lblText.Text = "Are you sure you want to close GChan? There are still threads/boards being tracke" + 50 | "d."; 51 | // 52 | // btnClose 53 | // 54 | this.btnClose.DialogResult = System.Windows.Forms.DialogResult.OK; 55 | this.btnClose.Dock = System.Windows.Forms.DockStyle.Fill; 56 | this.btnClose.Location = new System.Drawing.Point(0, 0); 57 | this.btnClose.Margin = new System.Windows.Forms.Padding(0, 0, 2, 0); 58 | this.btnClose.Name = "btnClose"; 59 | this.btnClose.Size = new System.Drawing.Size(199, 23); 60 | this.btnClose.TabIndex = 1; 61 | this.btnClose.Text = "Close Now"; 62 | this.btnClose.UseVisualStyleBackColor = true; 63 | this.btnClose.Click += new System.EventHandler(this.btnClose_Click); 64 | // 65 | // btnNoClose 66 | // 67 | this.btnNoClose.DialogResult = System.Windows.Forms.DialogResult.Cancel; 68 | this.btnNoClose.Dock = System.Windows.Forms.DockStyle.Fill; 69 | this.btnNoClose.Location = new System.Drawing.Point(203, 0); 70 | this.btnNoClose.Margin = new System.Windows.Forms.Padding(2, 0, 0, 0); 71 | this.btnNoClose.Name = "btnNoClose"; 72 | this.btnNoClose.Size = new System.Drawing.Size(200, 23); 73 | this.btnNoClose.TabIndex = 2; 74 | this.btnNoClose.Text = "Don\'t Close"; 75 | this.btnNoClose.UseVisualStyleBackColor = true; 76 | this.btnNoClose.Click += new System.EventHandler(this.btnNoClose_Click); 77 | // 78 | // chkWarning 79 | // 80 | this.chkWarning.AutoSize = true; 81 | this.chkWarning.Location = new System.Drawing.Point(20, 62); 82 | this.chkWarning.Name = "chkWarning"; 83 | this.chkWarning.Size = new System.Drawing.Size(167, 17); 84 | this.chkWarning.TabIndex = 3; 85 | this.chkWarning.Text = "Don\'t show this warning again"; 86 | this.chkWarning.UseVisualStyleBackColor = true; 87 | this.chkWarning.CheckedChanged += new System.EventHandler(this.chkWarning_CheckedChanged); 88 | // 89 | // chkSave 90 | // 91 | this.chkSave.AutoSize = true; 92 | this.chkSave.Location = new System.Drawing.Point(20, 39); 93 | this.chkSave.Name = "chkSave"; 94 | this.chkSave.Size = new System.Drawing.Size(283, 17); 95 | this.chkSave.TabIndex = 4; 96 | this.chkSave.Text = "Save threads and load them next time you start GChan"; 97 | this.chkSave.UseVisualStyleBackColor = true; 98 | this.chkSave.CheckedChanged += new System.EventHandler(this.chkSave_CheckedChanged); 99 | // 100 | // buttonTableLayoutPanel 101 | // 102 | this.buttonTableLayoutPanel.ColumnCount = 2; 103 | this.buttonTableLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); 104 | this.buttonTableLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); 105 | this.buttonTableLayoutPanel.Controls.Add(this.btnClose, 0, 0); 106 | this.buttonTableLayoutPanel.Controls.Add(this.btnNoClose, 1, 0); 107 | this.buttonTableLayoutPanel.Location = new System.Drawing.Point(15, 96); 108 | this.buttonTableLayoutPanel.Name = "buttonTableLayoutPanel"; 109 | this.buttonTableLayoutPanel.RowCount = 1; 110 | this.buttonTableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F)); 111 | this.buttonTableLayoutPanel.Size = new System.Drawing.Size(403, 23); 112 | this.buttonTableLayoutPanel.TabIndex = 5; 113 | // 114 | // CloseWarn 115 | // 116 | this.AcceptButton = this.btnClose; 117 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 118 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 119 | this.ClientSize = new System.Drawing.Size(435, 131); 120 | this.ControlBox = false; 121 | this.Controls.Add(this.buttonTableLayoutPanel); 122 | this.Controls.Add(this.chkSave); 123 | this.Controls.Add(this.chkWarning); 124 | this.Controls.Add(this.lblText); 125 | this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; 126 | this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); 127 | this.MaximumSize = new System.Drawing.Size(451, 170); 128 | this.MinimumSize = new System.Drawing.Size(451, 170); 129 | this.Name = "CloseWarn"; 130 | this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide; 131 | this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; 132 | this.Text = "Closing GChan"; 133 | this.TopMost = true; 134 | this.Load += new System.EventHandler(this.CloseWarn_Load); 135 | this.buttonTableLayoutPanel.ResumeLayout(false); 136 | this.ResumeLayout(false); 137 | this.PerformLayout(); 138 | 139 | } 140 | 141 | #endregion 142 | 143 | private System.Windows.Forms.TableLayoutPanel buttonTableLayoutPanel; 144 | } 145 | } -------------------------------------------------------------------------------- /GChan/Forms/GetStringMessageBox.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace GChan 2 | { 3 | partial class GetStringMessageBox 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(GetStringMessageBox)); 32 | this.promptLabel = new System.Windows.Forms.Label(); 33 | this.entryTextBox = new System.Windows.Forms.TextBox(); 34 | this.okButton = new System.Windows.Forms.Button(); 35 | this.cancelButton = new System.Windows.Forms.Button(); 36 | this.buttonTableLayoutPanel = new System.Windows.Forms.TableLayoutPanel(); 37 | this.buttonTableLayoutPanel.SuspendLayout(); 38 | this.SuspendLayout(); 39 | // 40 | // promptLabel 41 | // 42 | this.promptLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 43 | | System.Windows.Forms.AnchorStyles.Left) 44 | | System.Windows.Forms.AnchorStyles.Right))); 45 | this.promptLabel.AutoSize = true; 46 | this.promptLabel.Location = new System.Drawing.Point(82, 12); 47 | this.promptLabel.Name = "promptLabel"; 48 | this.promptLabel.Size = new System.Drawing.Size(271, 13); 49 | this.promptLabel.TabIndex = 0; 50 | this.promptLabel.Text = "Please enter a subject. Some characters are disallowed."; 51 | this.promptLabel.TextAlign = System.Drawing.ContentAlignment.TopCenter; 52 | // 53 | // entryTextBox 54 | // 55 | this.entryTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) 56 | | System.Windows.Forms.AnchorStyles.Right))); 57 | this.entryTextBox.Location = new System.Drawing.Point(16, 40); 58 | this.entryTextBox.Name = "entryTextBox"; 59 | this.entryTextBox.Size = new System.Drawing.Size(391, 20); 60 | this.entryTextBox.TabIndex = 1; 61 | this.entryTextBox.TextChanged += new System.EventHandler(this.entryTextBox_TextChanged); 62 | this.entryTextBox.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.entryTextBox_KeyPress); 63 | // 64 | // okButton 65 | // 66 | this.okButton.Dock = System.Windows.Forms.DockStyle.Fill; 67 | this.okButton.Location = new System.Drawing.Point(0, 0); 68 | this.okButton.Margin = new System.Windows.Forms.Padding(0, 0, 2, 0); 69 | this.okButton.Name = "okButton"; 70 | this.okButton.Size = new System.Drawing.Size(193, 23); 71 | this.okButton.TabIndex = 2; 72 | this.okButton.Text = "OK"; 73 | this.okButton.UseVisualStyleBackColor = true; 74 | this.okButton.Click += new System.EventHandler(this.okButton_Click); 75 | // 76 | // cancelButton 77 | // 78 | this.cancelButton.Dock = System.Windows.Forms.DockStyle.Fill; 79 | this.cancelButton.Location = new System.Drawing.Point(197, 0); 80 | this.cancelButton.Margin = new System.Windows.Forms.Padding(2, 0, 0, 0); 81 | this.cancelButton.Name = "cancelButton"; 82 | this.cancelButton.Size = new System.Drawing.Size(194, 23); 83 | this.cancelButton.TabIndex = 3; 84 | this.cancelButton.Text = "Cancel"; 85 | this.cancelButton.UseVisualStyleBackColor = true; 86 | this.cancelButton.Click += new System.EventHandler(this.cancelButton_Click); 87 | // 88 | // buttonTableLayoutPanel 89 | // 90 | this.buttonTableLayoutPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) 91 | | System.Windows.Forms.AnchorStyles.Right))); 92 | this.buttonTableLayoutPanel.ColumnCount = 2; 93 | this.buttonTableLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); 94 | this.buttonTableLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); 95 | this.buttonTableLayoutPanel.Controls.Add(this.cancelButton, 1, 0); 96 | this.buttonTableLayoutPanel.Controls.Add(this.okButton, 0, 0); 97 | this.buttonTableLayoutPanel.Location = new System.Drawing.Point(16, 66); 98 | this.buttonTableLayoutPanel.Name = "buttonTableLayoutPanel"; 99 | this.buttonTableLayoutPanel.RowCount = 1; 100 | this.buttonTableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F)); 101 | this.buttonTableLayoutPanel.Size = new System.Drawing.Size(391, 23); 102 | this.buttonTableLayoutPanel.TabIndex = 4; 103 | // 104 | // GetStringMessageBox 105 | // 106 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 107 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 108 | this.ClientSize = new System.Drawing.Size(419, 101); 109 | this.ControlBox = false; 110 | this.Controls.Add(this.entryTextBox); 111 | this.Controls.Add(this.promptLabel); 112 | this.Controls.Add(this.buttonTableLayoutPanel); 113 | this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); 114 | this.MaximumSize = new System.Drawing.Size(435, 140); 115 | this.MinimumSize = new System.Drawing.Size(435, 140); 116 | this.Name = "GetStringMessageBox"; 117 | this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; 118 | this.Text = "Subject"; 119 | this.buttonTableLayoutPanel.ResumeLayout(false); 120 | this.ResumeLayout(false); 121 | this.PerformLayout(); 122 | 123 | } 124 | 125 | #endregion 126 | 127 | private System.Windows.Forms.Label promptLabel; 128 | private System.Windows.Forms.TextBox entryTextBox; 129 | private System.Windows.Forms.Button okButton; 130 | private System.Windows.Forms.Button cancelButton; 131 | private System.Windows.Forms.TableLayoutPanel buttonTableLayoutPanel; 132 | } 133 | } -------------------------------------------------------------------------------- /GChan/Forms/GetStringMessageBox.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Windows.Forms; 4 | 5 | namespace GChan 6 | { 7 | public partial class GetStringMessageBox : Form 8 | { 9 | /// 10 | /// Get the user's entry into the textbox. 11 | /// 12 | public string UserEntry => entryTextBox.Text; 13 | 14 | public GetStringMessageBox(string defaultText = "") 15 | { 16 | InitializeComponent(); 17 | 18 | entryTextBox.Text = defaultText; 19 | } 20 | 21 | private void okButton_Click(object sender, EventArgs e) 22 | { 23 | DialogResult = DialogResult.OK; 24 | } 25 | 26 | private void cancelButton_Click(object sender, EventArgs e) 27 | { 28 | DialogResult = DialogResult.Cancel; 29 | } 30 | 31 | private void entryTextBox_KeyPress(object sender, KeyPressEventArgs e) 32 | { 33 | if (e.KeyChar == (char)Keys.Return) 34 | { 35 | okButton_Click(null, null); 36 | } 37 | else if (e.KeyChar == (char)Keys.Escape) 38 | { 39 | cancelButton_Click(null, null); 40 | } 41 | } 42 | 43 | private void entryTextBox_TextChanged(object sender, EventArgs e) 44 | { 45 | int prevSelectionStart = entryTextBox.SelectionStart; 46 | int prevSelectionLength = entryTextBox.SelectionLength; 47 | 48 | string originalText = entryTextBox.Text; 49 | string cleanedText = Utils.RemoveCharactersFromString(entryTextBox.Text, Utils.IllegalSubjectCharacters); 50 | 51 | entryTextBox.Text = cleanedText; 52 | 53 | if (cleanedText != originalText) 54 | { 55 | entryTextBox.SelectionStart = Math.Max(prevSelectionStart - 1, 0); 56 | entryTextBox.SelectionLength = Math.Max(prevSelectionLength - 1, 0); 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /GChan/Forms/SettingsForm.cs: -------------------------------------------------------------------------------- 1 | using GChan.Properties; 2 | using Microsoft.Win32; 3 | using Microsoft.WindowsAPICodePack.Dialogs; 4 | using System; 5 | using System.ComponentModel; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Windows.Forms; 9 | 10 | namespace GChan 11 | { 12 | public enum ImageFileNameFormat 13 | { 14 | [Description("ID (eg. '1570301.jpg')")] 15 | ID = 0, 16 | [Description("OriginalFilename (eg. 'LittleSaintJames.jpg')")] 17 | OriginalFilename = 1, 18 | [Description("ID - OriginalFilename (eg. '1570301 - LittleSaintJames.jpg')")] 19 | IDAndOriginalFilename = 2, 20 | [Description("OriginalFilename - ID (eg. 'LittleSaintJames.jpg - 1570301.jpg')")] 21 | OriginalFilenameAndID = 3 22 | }; 23 | 24 | public enum ThreadFolderNameFormat 25 | { 26 | [Description("ID - Subject")] 27 | IdSubject = 0, 28 | [Description("Subject - ID")] 29 | SubjectId = 1 30 | } 31 | 32 | public partial class SettingsForm : Form 33 | { 34 | string directory; 35 | 36 | public SettingsForm() 37 | { 38 | InitializeComponent(); 39 | //imageFilenameFormatComboBox.DataSource = EnumHelper.GetEnumDescriptions(typeof(ImageFileNameFormat)); 40 | imageFilenameFormatComboBox.DataSource = Enum.GetValues(typeof(ImageFileNameFormat)) 41 | .Cast() 42 | .Select(value => new 43 | { 44 | EnumDescription = EnumHelper.GetEnumDescription(value), 45 | EnumValue = value 46 | }) 47 | .ToList(); 48 | 49 | //threadFolderNameFormatComboBox.DataSource = EnumHelper.GetEnumDescriptions(typeof(ThreadFolderNameFormat)); 50 | threadFolderNameFormatComboBox.DataSource = Enum.GetValues(typeof(ThreadFolderNameFormat)) 51 | .Cast() 52 | .Select(value => new 53 | { 54 | EnumDescription = EnumHelper.GetEnumDescription(value), 55 | EnumValue = value 56 | }) 57 | .ToList(); 58 | } 59 | 60 | /// 61 | /// Load settings into controls. 62 | /// 63 | private void Settings_Shown(object sender, EventArgs e) 64 | { 65 | userAgentTextBox.Text = Settings.Default.UserAgent; 66 | 67 | directory = Settings.Default.SavePath; 68 | directoryTextBox.Text = directory; 69 | 70 | timerNumeric.Value = (Settings.Default.ScanTimer / 1000); 71 | concurrentDownloadsNumeric.Value = Settings.Default.MaximumConcurrentDownloads; 72 | 73 | imageFilenameFormatComboBox.SelectedIndex = Settings.Default.ImageFilenameFormat; 74 | threadFolderNameFormatComboBox.SelectedIndex = Settings.Default.ThreadFolderNameFormat; 75 | 76 | chkSaveHtml.Checked = Settings.Default.SaveHtml; 77 | chkSaveThumbnails.Checked = Settings.Default.SaveThumbnails; 78 | chkSave.Checked = Settings.Default.SaveListsOnClose; 79 | chkTray.Checked = Settings.Default.MinimizeToTray; 80 | chkWarn.Checked = Settings.Default.WarnOnClose; 81 | 82 | chkStartWithWindows.Checked = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true).GetValueNames().Contains(Utils.PROGRAM_NAME); 83 | chkStartWithWindowsMinimized.Checked = Settings.Default.StartWithWindowsMinimized; 84 | 85 | renameThreadFolderCheckBox.Checked = Settings.Default.AddThreadSubjectToFolder; 86 | 87 | addUrlFromClipboardWhenTextboxEmpty.Checked = Settings.Default.AddUrlFromClipboardWhenTextboxEmpty; 88 | 89 | checkForUpdatesOnStartCheckBox.Checked = Settings.Default.CheckForUpdatesOnStart; 90 | } 91 | 92 | private void buttonSave_Click(object sender, EventArgs e) 93 | { 94 | if (!HasWriteAccessToFolder(directory, out var reason)) 95 | { 96 | MessageBox.Show(reason, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); 97 | return; 98 | } 99 | 100 | if (timerNumeric.Value < 5) 101 | { 102 | MessageBox.Show("Timer must be greater than 5 seconds."); 103 | return; 104 | } 105 | 106 | SaveSettings(); 107 | Close(); 108 | } 109 | 110 | private void buttonCancel_Click(object sender, EventArgs e) 111 | { 112 | Close(); 113 | } 114 | 115 | private void SaveSettings() 116 | { 117 | Settings.Default.UserAgent = userAgentTextBox.Text; 118 | Settings.Default.SavePath = directory; 119 | Settings.Default.ScanTimer = (int)timerNumeric.Value * 1000; 120 | Settings.Default.MaximumConcurrentDownloads = (int)concurrentDownloadsNumeric.Value; 121 | Settings.Default.ImageFilenameFormat = (byte)imageFilenameFormatComboBox.SelectedIndex; 122 | Settings.Default.ThreadFolderNameFormat = (byte)threadFolderNameFormatComboBox.SelectedIndex; 123 | Settings.Default.SaveHtml = chkSaveHtml.Checked; 124 | Settings.Default.SaveThumbnails = chkSaveThumbnails.Checked; 125 | Settings.Default.SaveListsOnClose = chkSave.Checked; 126 | Settings.Default.MinimizeToTray = chkTray.Checked; 127 | Settings.Default.WarnOnClose = chkWarn.Checked; 128 | Settings.Default.StartWithWindowsMinimized = chkStartWithWindowsMinimized.Checked; 129 | Settings.Default.AddThreadSubjectToFolder = renameThreadFolderCheckBox.Checked; 130 | Settings.Default.AddUrlFromClipboardWhenTextboxEmpty = addUrlFromClipboardWhenTextboxEmpty.Checked; 131 | Settings.Default.CheckForUpdatesOnStart = checkForUpdatesOnStartCheckBox.Checked; 132 | 133 | Settings.Default.Save(); 134 | 135 | var registryKey = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true); 136 | if (chkStartWithWindows.Checked) 137 | { 138 | var args = (Settings.Default.MinimizeToTray && Settings.Default.StartWithWindowsMinimized) ? $" {Program.TRAY_CMDLINE_ARG}" : ""; 139 | registryKey.SetValue( 140 | Utils.PROGRAM_NAME, 141 | '"' + Application.ExecutablePath + '"' + args 142 | ); 143 | } 144 | else if (registryKey.GetValue(Utils.PROGRAM_NAME) != null) 145 | { 146 | registryKey.DeleteValue(Utils.PROGRAM_NAME); 147 | } 148 | } 149 | 150 | //Source: https://stackoverflow.com/q/1410127/8306962 151 | private bool HasWriteAccessToFolder(string folderPath, out string reason) 152 | { 153 | try 154 | { 155 | // Attempt to get a list of security permissions from the folder. 156 | // This will raise an exception if the path is read only or do not have access to view the permissions. 157 | var directorySecurity = Directory.GetAccessControl(folderPath); 158 | reason = "No problem"; 159 | return true; 160 | } 161 | catch (UnauthorizedAccessException) 162 | { 163 | reason = "No permission to write on that location."; 164 | return false; 165 | } 166 | catch (DirectoryNotFoundException) 167 | { 168 | reason = "Directory not found."; 169 | return false; 170 | } 171 | } 172 | 173 | private void SetPathButton_Click(object sender, EventArgs e) 174 | { 175 | var openFileDialog = new CommonOpenFileDialog(); 176 | openFileDialog.IsFolderPicker = true; 177 | openFileDialog.Title = "Select Folder"; 178 | openFileDialog.InitialDirectory = Path.GetPathRoot(Environment.SystemDirectory); 179 | 180 | if (openFileDialog.ShowDialog() == CommonFileDialogResult.Ok && !string.IsNullOrWhiteSpace(openFileDialog.FileName)) 181 | { 182 | directory = openFileDialog.FileName; 183 | directoryTextBox.Text = directory; 184 | } 185 | } 186 | 187 | private void textBox1_DoubleClick(object sender, EventArgs e) 188 | { 189 | System.Diagnostics.Process.Start("explorer.exe", string.Format(directory)); 190 | } 191 | 192 | private void chkTray_CheckedChanged(object sender, EventArgs e) 193 | { 194 | EnableChkStartWithWindowsMinimizedCheckBox(); 195 | } 196 | 197 | private void chkStartWithWindows_CheckedChanged(object sender, EventArgs e) 198 | { 199 | EnableChkStartWithWindowsMinimizedCheckBox(); 200 | } 201 | 202 | private void EnableChkStartWithWindowsMinimizedCheckBox() 203 | { 204 | chkStartWithWindowsMinimized.Enabled = chkTray.Checked && chkStartWithWindows.Checked; 205 | } 206 | 207 | private void renameThreadFolderCheckBox_CheckedChanged(object sender, EventArgs e) 208 | { 209 | threadFolderNameFormatLabel.Enabled = renameThreadFolderCheckBox.Checked; 210 | threadFolderNameFormatComboBox.Enabled = renameThreadFolderCheckBox.Checked; 211 | } 212 | 213 | private void chkHTML_CheckedChanged(object sender, EventArgs e) 214 | { 215 | chkSaveThumbnails.Enabled = chkSaveHtml.Checked; 216 | } 217 | } 218 | } -------------------------------------------------------------------------------- /GChan/Forms/SettingsForm.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 22, 20 122 | 123 | 124 | 22, 20 125 | 126 | 127 | When a thread is automatically or manually removed from GChan's tracking list, if this option 128 | is enabled GChan will rename thread's folder using the desired format selected below. 129 | If the thread has a custom subject set then that is used instead. 130 | 131 | 132 | The maximum amount of files allowed to be downloading at once. 133 | If set too high, sites (e.g. 4chan) can rate limit your requests. 134 | A high amount can effect your network (e.g. gaming latency). 135 | Each download uses a thread, which may effect system performance. 136 | 137 | 138 | User-Agent headers are sent with every scraping request. The default value should be fine in most cases but if GChan is getting blocked you may wish to change it to the User-Agent header your browser uses. You should only need to mess with this if you know what you are doing. 139 | 140 | 141 | 61 142 | 143 | -------------------------------------------------------------------------------- /GChan/Forms/UpdateInfoForm.cs: -------------------------------------------------------------------------------- 1 | using GChan.Controllers; 2 | using System; 3 | using System.Diagnostics; 4 | using System.Windows.Forms; 5 | 6 | namespace GChan.Forms 7 | { 8 | public partial class UpdateInfoForm : Form 9 | { 10 | public UpdateInfoForm() 11 | { 12 | InitializeComponent(); 13 | 14 | UpdateController.Instance.Progress.ProgressChanged += Progress_ProgressChanged; 15 | 16 | infoLabel.Text = infoLabel.Text.Replace("%currentversion%", $"v{UpdateController.Instance.CurrentVersion}"); 17 | infoLabel.Text = infoLabel.Text.Replace("%latestversion%", $"v{UpdateController.Instance.CheckForUpdatesResult.LastVersion}"); 18 | } 19 | 20 | private void Progress_ProgressChanged(object sender, double e) 21 | { 22 | Invoke((MethodInvoker)delegate () 23 | { 24 | progressBar.Value = (int)(e * 100); 25 | }); 26 | } 27 | 28 | private void viewReleasesButton_Click(object sender, EventArgs e) 29 | { 30 | Process.Start("https://github.com/" + Program.GITHUB_REPOSITORY_OWNER + "/" + Program.GITHUB_REPOSITORY_NAME + "/releases"); 31 | } 32 | 33 | private void doNotUpdateButton_Click(object sender, EventArgs e) 34 | { 35 | Close(); 36 | } 37 | 38 | private async void updateButton_Click(object sender, EventArgs e) 39 | { 40 | buttonsPanel.Hide(); 41 | downloadingPanel.Show(); 42 | await UpdateController.Instance.PerformUpdate(); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /GChan/Helpers/EnumHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.ComponentModel; 5 | 6 | namespace GChan 7 | { 8 | public static class EnumHelper 9 | { 10 | public static string GetEnumDescription(Enum value) 11 | { 12 | var fi = value.GetType().GetField(value.ToString()); 13 | var attributes = fi.GetCustomAttributes(typeof(DescriptionAttribute), false); 14 | 15 | if (attributes.Length > 0) 16 | return ((DescriptionAttribute)attributes[0]).Description; 17 | else 18 | return value.ToString(); 19 | } 20 | 21 | public static T GetEnumFromDescription(string stringValue) where T : Enum 22 | { 23 | foreach (object e in Enum.GetValues(typeof(T))) 24 | if (GetEnumDescription((Enum)e).Equals(stringValue)) 25 | return (T)e; 26 | 27 | throw new ArgumentException("No matching enum value found."); 28 | } 29 | 30 | public static IEnumerable GetEnumDescriptions(Type enumType) 31 | { 32 | var strings = new Collection(); 33 | 34 | foreach (Enum e in Enum.GetValues(enumType)) 35 | strings.Add(GetEnumDescription(e)); 36 | 37 | return strings; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /GChan/Helpers/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using GChan.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace GChan.Helpers 6 | { 7 | public static class EnumerableExtensions 8 | { 9 | public static void ForEach(this IEnumerable source, Action action) 10 | { 11 | foreach (var item in source) 12 | { 13 | action(item); 14 | } 15 | } 16 | 17 | /// 18 | /// If is false then remove items from if they are already in . 19 | /// 20 | public static IEnumerable MaybeRemoveAlreadySavedLinks( 21 | this IEnumerable assets, 22 | bool includeAlreadySaved, 23 | SavedIdsCollection savedIds 24 | ) 25 | { 26 | foreach (var link in assets) 27 | { 28 | var alreadySaved = savedIds.Contains(link.Tim); 29 | 30 | if (includeAlreadySaved || !alreadySaved) 31 | { 32 | yield return link; 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /GChan/Helpers/ExceptionExtensions.cs: -------------------------------------------------------------------------------- 1 | using GChan.Trackers; 2 | using System; 3 | using System.Linq; 4 | using System.Net; 5 | 6 | namespace GChan.Helpers 7 | { 8 | public static class ExceptionExtensions 9 | { 10 | /// 11 | /// Does the indicate content is no longer available (404 / 410 status code). 12 | /// 13 | /// If the exception comes from a http response, is set to the cast of . 14 | public static bool IsGone(this WebException exception, out HttpWebResponse httpWebResponse) 15 | { 16 | var isProtocolError = exception.Status == WebExceptionStatus.ProtocolError; 17 | httpWebResponse = exception.Response as HttpWebResponse; 18 | var isGoneStatusCode = Tracker.GoneStatusCodes.Contains(httpWebResponse?.StatusCode); 19 | 20 | return isProtocolError && (httpWebResponse != null && isGoneStatusCode); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /GChan/Models/ConcurrentHashSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | 6 | namespace GChan.Models 7 | { 8 | /// 9 | /// Credit: Ben Mosher https://stackoverflow.com/a/18923091/8306962 10 | /// 11 | public class ConcurrentHashSet : IDisposable 12 | { 13 | protected readonly ReaderWriterLockSlim locker = new(LockRecursionPolicy.SupportsRecursion); 14 | protected readonly HashSet set = new(); 15 | 16 | public int Count 17 | { 18 | get 19 | { 20 | locker.EnterReadLock(); 21 | 22 | try 23 | { 24 | return set.Count; 25 | } 26 | finally 27 | { 28 | if (locker.IsReadLockHeld) 29 | { 30 | locker.ExitReadLock(); 31 | } 32 | } 33 | } 34 | } 35 | 36 | public bool Add(T item) 37 | { 38 | locker.EnterWriteLock(); 39 | 40 | try 41 | { 42 | return set.Add(item); 43 | } 44 | finally 45 | { 46 | if (locker.IsWriteLockHeld) 47 | { 48 | locker.ExitWriteLock(); 49 | } 50 | } 51 | } 52 | 53 | public void Clear() 54 | { 55 | locker.EnterWriteLock(); 56 | try 57 | { 58 | set.Clear(); 59 | } 60 | finally 61 | { 62 | if (locker.IsWriteLockHeld) 63 | { 64 | locker.ExitWriteLock(); 65 | } 66 | } 67 | } 68 | 69 | public bool Contains(T item) 70 | { 71 | locker.EnterReadLock(); 72 | 73 | try 74 | { 75 | return set.Contains(item); 76 | } 77 | finally 78 | { 79 | if (locker.IsReadLockHeld) 80 | { 81 | locker.ExitReadLock(); 82 | } 83 | } 84 | } 85 | 86 | public bool Remove(T item) 87 | { 88 | locker.EnterWriteLock(); 89 | 90 | try 91 | { 92 | return set.Remove(item); 93 | } 94 | finally 95 | { 96 | if (locker.IsWriteLockHeld) 97 | { 98 | locker.ExitWriteLock(); 99 | } 100 | } 101 | } 102 | 103 | public T[] ToArray() 104 | { 105 | locker.EnterReadLock(); 106 | 107 | try 108 | { 109 | return set.ToArray(); 110 | } 111 | finally 112 | { 113 | if (locker.IsReadLockHeld) 114 | { 115 | locker.ExitReadLock(); 116 | } 117 | } 118 | } 119 | 120 | public void Dispose() 121 | { 122 | Dispose(true); 123 | GC.SuppressFinalize(this); 124 | } 125 | 126 | protected virtual void Dispose(bool disposing) 127 | { 128 | if (disposing) 129 | { 130 | locker?.Dispose(); 131 | } 132 | } 133 | 134 | ~ConcurrentHashSet() 135 | { 136 | Dispose(false); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /GChan/Models/MainFormModel.cs: -------------------------------------------------------------------------------- 1 | using GChan.Controls; 2 | using GChan.Forms; 3 | using GChan.Properties; 4 | using GChan.Trackers; 5 | using NLog; 6 | using System.ComponentModel; 7 | using System.Runtime.CompilerServices; 8 | 9 | namespace GChan.Models 10 | { 11 | class MainFormModel : INotifyPropertyChanged 12 | { 13 | MainForm form; 14 | 15 | public event PropertyChangedEventHandler PropertyChanged; 16 | 17 | public void NotifyPropertyChanged([CallerMemberName] string propertyName = "") 18 | { 19 | #if DEBUG 20 | logger.Trace($"NotifyPropertyChanged! propertyName: {propertyName}."); 21 | #endif 22 | 23 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 24 | } 25 | 26 | public SortableBindingList Threads { get; set; } = new(); 27 | 28 | public SortableBindingList Boards { get; set; } = new(); 29 | 30 | public string ThreadsTabText => $"Threads ({Threads.Count})"; 31 | 32 | public string BoardsTabText => $"Boards ({Boards.Count})"; 33 | 34 | public string NotificationTrayTooltip { 35 | get { 36 | return $"Scraping {Threads.Count} thread{(Threads.Count != 1 ? "s" : "")} and {Boards.Count} board{(Boards.Count != 1 ? "s" : "")} every {Settings.Default.ScanTimer / 60 / 1000} minute{((Settings.Default.ScanTimer / 60 / 1000) != 1 ? "s" : "")}." + 37 | "\nClick to show/hide."; 38 | } 39 | } 40 | 41 | private readonly ILogger logger = LogManager.GetCurrentClassLogger(); 42 | 43 | public MainFormModel(MainForm form) 44 | { 45 | this.form = form; 46 | 47 | Threads.ListChanged += Threads_ListChanged; 48 | Boards.ListChanged += Boards_ListChanged; 49 | } 50 | 51 | private void Threads_ListChanged(object sender, ListChangedEventArgs e) 52 | { 53 | form.threadsTabPage.Text = ThreadsTabText; 54 | } 55 | 56 | private void Boards_ListChanged(object sender, ListChangedEventArgs e) 57 | { 58 | NotifyPropertyChanged(nameof(Boards)); 59 | form.boardsTabPage.Text = BoardsTabText; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /GChan/Models/SavedIdsCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace GChan.Models 5 | { 6 | /// 7 | /// Thread safe collection of s. For saving downloaded image ids. 8 | /// 9 | public class SavedIdsCollection : ConcurrentHashSet 10 | { 11 | public SavedIdsCollection() 12 | { 13 | 14 | } 15 | 16 | /// Comma delimited list of ints. 17 | public SavedIdsCollection(string list) 18 | { 19 | LoadStringList(list); 20 | } 21 | 22 | /// Comma delimited list of ints. 23 | public void LoadStringList(string list) 24 | { 25 | var splits = list.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 26 | var items = splits.Select(long.Parse); 27 | 28 | locker.EnterWriteLock(); 29 | 30 | try 31 | { 32 | foreach (var item in items) 33 | { 34 | set.Add(item); 35 | } 36 | } 37 | finally 38 | { 39 | if (locker.IsWriteLockHeld) 40 | { 41 | locker.ExitWriteLock(); 42 | } 43 | } 44 | } 45 | 46 | public string ToStringList() 47 | { 48 | var array = ToArray(); 49 | return string.Join(",", array); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /GChan/Program.cs: -------------------------------------------------------------------------------- 1 | using GChan.Data; 2 | using GChan.Forms; 3 | using GChan.Properties; 4 | using NLog; 5 | using System; 6 | using System.IO; 7 | using System.Runtime.InteropServices; 8 | using System.Threading; 9 | using System.Windows.Forms; 10 | 11 | namespace GChan 12 | { 13 | internal static class Program 14 | { 15 | public static MainForm mainForm; 16 | public static string APPLICATION_INSTALL_DIRECTORY { get; } = AppDomain.CurrentDomain.BaseDirectory; 17 | 18 | #if DEBUG 19 | public static string PROGRAM_DATA_PATH => Path.Combine(Path.GetDirectoryName(Application.CommonAppDataPath), "DEBUG"); 20 | #else 21 | public static string PROGRAM_DATA_PATH => Path.GetDirectoryName(Application.CommonAppDataPath); 22 | #endif 23 | 24 | public const string NAME = "GChan"; 25 | 26 | public const string GITHUB_REPOSITORY_OWNER = "Issung"; 27 | 28 | #if DEBUG 29 | public const string GITHUB_REPOSITORY_NAME = "GChanUpdateTesting"; 30 | #else 31 | public const string GITHUB_REPOSITORY_NAME = "GChan"; 32 | #endif 33 | 34 | public const string TRAY_CMDLINE_ARG = "-tray"; 35 | 36 | public static string[] arguments; 37 | 38 | private static readonly ILogger logger = LogManager.GetCurrentClassLogger(); 39 | 40 | /// 41 | /// The main entry point for the application. 42 | /// 43 | [STAThread] 44 | private static void Main(string[] args) 45 | { 46 | arguments = args; 47 | 48 | #if DEBUG 49 | Directory.CreateDirectory(PROGRAM_DATA_PATH); 50 | #endif 51 | 52 | if (Settings.Default.UpdateSettings) 53 | { 54 | Settings.Default.Upgrade(); 55 | Settings.Default.Reload(); 56 | Settings.Default.UpdateSettings = false; 57 | Settings.Default.Save(); 58 | } 59 | 60 | // See if an instance is already running... 61 | // Code from https://stackoverflow.com/a/1777704 by Matt Davis (https://stackoverflow.com/users/51170/matt-davis) 62 | if (_single.WaitOne(TimeSpan.Zero, true)) 63 | { 64 | // Unhandled exceptions for our Application Domain 65 | AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(AppDomain_UnhandledException); 66 | 67 | // Unhandled exceptions for the executing UI thread 68 | Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException); 69 | 70 | Application.EnableVisualStyles(); 71 | Application.SetCompatibleTextRenderingDefault(false); 72 | 73 | try 74 | { 75 | mainForm = new MainForm(); 76 | Application.Run(mainForm); 77 | } 78 | catch 79 | { 80 | // handle exception accordingly 81 | } 82 | finally 83 | { 84 | _single.ReleaseMutex(); 85 | } 86 | } 87 | else 88 | { 89 | // Bring already open instance to top and activate it. 90 | NativeMethods.PostMessage( 91 | (IntPtr)HWND_BROADCAST, 92 | WM_MY_MSG, 93 | new IntPtr(0xCDCD), 94 | new IntPtr(0xEFEF) 95 | ); 96 | } 97 | } 98 | 99 | /// 100 | /// Main thread exception handler 101 | /// 102 | public static void Application_ThreadException(object sender, ThreadExceptionEventArgs exceptionArgs) 103 | { 104 | Settings.Default.SaveListsOnClose = true; 105 | Settings.Default.Save(); 106 | 107 | try 108 | { 109 | DataController.SaveAll(mainForm.Model.Threads, mainForm.Model.Boards); 110 | logger.Error(exceptionArgs.Exception, $"Application_ThreadException."); 111 | } 112 | catch (Exception ex) 113 | { 114 | var aggregate = new AggregateException(ex, exceptionArgs.Exception); 115 | logger.Fatal(aggregate, "Thread Exception."); 116 | MessageBox.Show(ex.Message); 117 | } 118 | } 119 | 120 | /// 121 | /// Application domain exception handler 122 | /// 123 | public static void AppDomain_UnhandledException(object sender, UnhandledExceptionEventArgs args) 124 | { 125 | Settings.Default.SaveListsOnClose = true; 126 | Settings.Default.Save(); 127 | 128 | try 129 | { 130 | DataController.SaveAll(mainForm.Model.Threads, mainForm.Model.Boards); 131 | Exception argsException = args.ExceptionObject as Exception; 132 | logger.Error(argsException, $"AppDomain_UnhandledException."); 133 | } 134 | catch (Exception ex) 135 | { 136 | var aggregate = new AggregateException(ex, args.ExceptionObject as Exception); 137 | logger.Fatal(aggregate, "Unhandled Exception."); 138 | MessageBox.Show(ex.Message); 139 | } 140 | } 141 | 142 | #region Dll Imports 143 | 144 | private const int HWND_BROADCAST = 0xFFFF; 145 | public static readonly int WM_MY_MSG = NativeMethods.RegisterWindowMessage("WM_MY_MSG"); 146 | private static readonly Mutex _single = new Mutex(true, "GChanRunning"); 147 | 148 | private class NativeMethods 149 | { 150 | [DllImport("user32")] 151 | public static extern bool PostMessage(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam); 152 | 153 | [DllImport("user32", CharSet = CharSet.Unicode)] 154 | public static extern int RegisterWindowMessage(string message); 155 | } 156 | 157 | #endregion Dll Imports 158 | } 159 | } -------------------------------------------------------------------------------- /GChan/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // Allgemeine Informationen über eine Assembly werden über die folgenden 5 | // Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, 6 | // die mit einer Assembly verknüpft sind. 7 | [assembly: AssemblyTitle("GChan")] 8 | [assembly: AssemblyDescription("GChan - The Imageboard Scraper")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("GChan")] 12 | [assembly: AssemblyCopyright("Copyright ©Issung")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Durch Festlegen von ComVisible auf "false" werden die Typen in dieser Assembly unsichtbar 17 | // für COM-Komponenten. Wenn Sie auf einen Typ in dieser Assembly von 18 | // COM zugreifen müssen, legen Sie das ComVisible-Attribut für diesen Typ auf "true" fest. 19 | [assembly: ComVisible(false)] 20 | 21 | // Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird 22 | [assembly: Guid("59c5df69-e5ad-4fa2-ae88-8db77a8d9406")] 23 | 24 | // Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: 25 | // 26 | // Hauptversion 27 | // Nebenversion 28 | // Buildnummer 29 | // Revision 30 | // 31 | // Sie können alle Werte angeben oder die standardmäßigen Build- und Revisionsnummern 32 | // übernehmen, indem Sie "*" eingeben: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("5.5.0")] 35 | [assembly: AssemblyFileVersion("5.5.0")] 36 | -------------------------------------------------------------------------------- /GChan/Properties/DataSources/GChan.Models.MainFormModel.datasource: -------------------------------------------------------------------------------- 1 |  2 | 8 | 9 | GChan.Models.MainFormModel, GChan, Version=3.3.0.0, Culture=neutral, PublicKeyToken=null 10 | -------------------------------------------------------------------------------- /GChan/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace GChan.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GChan.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized resource of type System.Drawing.Bitmap. 65 | /// 66 | internal static System.Drawing.Bitmap alert { 67 | get { 68 | object obj = ResourceManager.GetObject("alert", resourceCulture); 69 | return ((System.Drawing.Bitmap)(obj)); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized resource of type System.Drawing.Bitmap. 75 | /// 76 | internal static System.Drawing.Bitmap clipboard { 77 | get { 78 | object obj = ResourceManager.GetObject("clipboard", resourceCulture); 79 | return ((System.Drawing.Bitmap)(obj)); 80 | } 81 | } 82 | 83 | /// 84 | /// Looks up a localized resource of type System.Drawing.Bitmap. 85 | /// 86 | internal static System.Drawing.Bitmap close { 87 | get { 88 | object obj = ResourceManager.GetObject("close", resourceCulture); 89 | return ((System.Drawing.Bitmap)(obj)); 90 | } 91 | } 92 | 93 | /// 94 | /// Looks up a localized resource of type System.Drawing.Bitmap. 95 | /// 96 | internal static System.Drawing.Bitmap download { 97 | get { 98 | object obj = ResourceManager.GetObject("download", resourceCulture); 99 | return ((System.Drawing.Bitmap)(obj)); 100 | } 101 | } 102 | 103 | /// 104 | /// Looks up a localized resource of type System.Drawing.Bitmap. 105 | /// 106 | internal static System.Drawing.Bitmap file { 107 | get { 108 | object obj = ResourceManager.GetObject("file", resourceCulture); 109 | return ((System.Drawing.Bitmap)(obj)); 110 | } 111 | } 112 | 113 | /// 114 | /// Looks up a localized resource of type System.Drawing.Bitmap. 115 | /// 116 | internal static System.Drawing.Bitmap folder { 117 | get { 118 | object obj = ResourceManager.GetObject("folder", resourceCulture); 119 | return ((System.Drawing.Bitmap)(obj)); 120 | } 121 | } 122 | 123 | /// 124 | /// Looks up a localized resource of type System.Drawing.Bitmap. 125 | /// 126 | internal static System.Drawing.Bitmap question { 127 | get { 128 | object obj = ResourceManager.GetObject("question", resourceCulture); 129 | return ((System.Drawing.Bitmap)(obj)); 130 | } 131 | } 132 | 133 | /// 134 | /// Looks up a localized resource of type System.Drawing.Bitmap. 135 | /// 136 | internal static System.Drawing.Bitmap Rename { 137 | get { 138 | object obj = ResourceManager.GetObject("Rename", resourceCulture); 139 | return ((System.Drawing.Bitmap)(obj)); 140 | } 141 | } 142 | 143 | /// 144 | /// Looks up a localized resource of type System.Drawing.Bitmap. 145 | /// 146 | internal static System.Drawing.Bitmap settings_wrench { 147 | get { 148 | object obj = ResourceManager.GetObject("settings-wrench", resourceCulture); 149 | return ((System.Drawing.Bitmap)(obj)); 150 | } 151 | } 152 | 153 | /// 154 | /// Looks up a localized resource of type System.Drawing.Bitmap. 155 | /// 156 | internal static System.Drawing.Bitmap world { 157 | get { 158 | object obj = ResourceManager.GetObject("world", resourceCulture); 159 | return ((System.Drawing.Bitmap)(obj)); 160 | } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /GChan/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | ..\icons\Rename.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 123 | 124 | 125 | ..\icons\clipboard.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 126 | 127 | 128 | ..\icons\question.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 129 | 130 | 131 | ..\icons\close.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 132 | 133 | 134 | ..\icons\settings-wrench.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 135 | 136 | 137 | ..\icons\world.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 138 | 139 | 140 | ..\icons\alert.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 141 | 142 | 143 | ..\icons\folder.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 144 | 145 | 146 | ..\icons\download.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 147 | 148 | 149 | ..\Icons\file.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 150 | 151 | -------------------------------------------------------------------------------- /GChan/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace GChan.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.9.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | 26 | [global::System.Configuration.UserScopedSettingAttribute()] 27 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 28 | [global::System.Configuration.DefaultSettingValueAttribute("C:\\GChan")] 29 | public string SavePath { 30 | get { 31 | return ((string)(this["SavePath"])); 32 | } 33 | set { 34 | this["SavePath"] = value; 35 | } 36 | } 37 | 38 | [global::System.Configuration.UserScopedSettingAttribute()] 39 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 40 | [global::System.Configuration.DefaultSettingValueAttribute("60000")] 41 | public int ScanTimer { 42 | get { 43 | return ((int)(this["ScanTimer"])); 44 | } 45 | set { 46 | this["ScanTimer"] = value; 47 | } 48 | } 49 | 50 | [global::System.Configuration.UserScopedSettingAttribute()] 51 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 52 | [global::System.Configuration.DefaultSettingValueAttribute("False")] 53 | public bool SaveHtml { 54 | get { 55 | return ((bool)(this["SaveHtml"])); 56 | } 57 | set { 58 | this["SaveHtml"] = value; 59 | } 60 | } 61 | 62 | [global::System.Configuration.UserScopedSettingAttribute()] 63 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 64 | [global::System.Configuration.DefaultSettingValueAttribute("True")] 65 | public bool IsFirstStart { 66 | get { 67 | return ((bool)(this["IsFirstStart"])); 68 | } 69 | set { 70 | this["IsFirstStart"] = value; 71 | } 72 | } 73 | 74 | [global::System.Configuration.UserScopedSettingAttribute()] 75 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 76 | [global::System.Configuration.DefaultSettingValueAttribute("False")] 77 | public bool SaveListsOnClose { 78 | get { 79 | return ((bool)(this["SaveListsOnClose"])); 80 | } 81 | set { 82 | this["SaveListsOnClose"] = value; 83 | } 84 | } 85 | 86 | [global::System.Configuration.UserScopedSettingAttribute()] 87 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 88 | [global::System.Configuration.DefaultSettingValueAttribute("True")] 89 | public bool MinimizeToTray { 90 | get { 91 | return ((bool)(this["MinimizeToTray"])); 92 | } 93 | set { 94 | this["MinimizeToTray"] = value; 95 | } 96 | } 97 | 98 | [global::System.Configuration.UserScopedSettingAttribute()] 99 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 100 | [global::System.Configuration.DefaultSettingValueAttribute("True")] 101 | public bool WarnOnClose { 102 | get { 103 | return ((bool)(this["WarnOnClose"])); 104 | } 105 | set { 106 | this["WarnOnClose"] = value; 107 | } 108 | } 109 | 110 | [global::System.Configuration.UserScopedSettingAttribute()] 111 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 112 | [global::System.Configuration.DefaultSettingValueAttribute("True")] 113 | public bool UpdateSettings { 114 | get { 115 | return ((bool)(this["UpdateSettings"])); 116 | } 117 | set { 118 | this["UpdateSettings"] = value; 119 | } 120 | } 121 | 122 | [global::System.Configuration.UserScopedSettingAttribute()] 123 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 124 | [global::System.Configuration.DefaultSettingValueAttribute("False")] 125 | public bool AddThreadSubjectToFolder { 126 | get { 127 | return ((bool)(this["AddThreadSubjectToFolder"])); 128 | } 129 | set { 130 | this["AddThreadSubjectToFolder"] = value; 131 | } 132 | } 133 | 134 | [global::System.Configuration.UserScopedSettingAttribute()] 135 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 136 | [global::System.Configuration.DefaultSettingValueAttribute("0")] 137 | public byte ImageFilenameFormat { 138 | get { 139 | return ((byte)(this["ImageFilenameFormat"])); 140 | } 141 | set { 142 | this["ImageFilenameFormat"] = value; 143 | } 144 | } 145 | 146 | [global::System.Configuration.UserScopedSettingAttribute()] 147 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 148 | [global::System.Configuration.DefaultSettingValueAttribute("0")] 149 | public byte ThreadFolderNameFormat { 150 | get { 151 | return ((byte)(this["ThreadFolderNameFormat"])); 152 | } 153 | set { 154 | this["ThreadFolderNameFormat"] = value; 155 | } 156 | } 157 | 158 | [global::System.Configuration.UserScopedSettingAttribute()] 159 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 160 | [global::System.Configuration.DefaultSettingValueAttribute("False")] 161 | public bool AddUrlFromClipboardWhenTextboxEmpty { 162 | get { 163 | return ((bool)(this["AddUrlFromClipboardWhenTextboxEmpty"])); 164 | } 165 | set { 166 | this["AddUrlFromClipboardWhenTextboxEmpty"] = value; 167 | } 168 | } 169 | 170 | [global::System.Configuration.UserScopedSettingAttribute()] 171 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 172 | [global::System.Configuration.DefaultSettingValueAttribute("False")] 173 | public bool StartWithWindowsMinimized { 174 | get { 175 | return ((bool)(this["StartWithWindowsMinimized"])); 176 | } 177 | set { 178 | this["StartWithWindowsMinimized"] = value; 179 | } 180 | } 181 | 182 | [global::System.Configuration.UserScopedSettingAttribute()] 183 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 184 | [global::System.Configuration.DefaultSettingValueAttribute("True")] 185 | public bool CheckForUpdatesOnStart { 186 | get { 187 | return ((bool)(this["CheckForUpdatesOnStart"])); 188 | } 189 | set { 190 | this["CheckForUpdatesOnStart"] = value; 191 | } 192 | } 193 | 194 | [global::System.Configuration.UserScopedSettingAttribute()] 195 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 196 | [global::System.Configuration.DefaultSettingValueAttribute("35")] 197 | public int MaximumConcurrentDownloads { 198 | get { 199 | return ((int)(this["MaximumConcurrentDownloads"])); 200 | } 201 | set { 202 | this["MaximumConcurrentDownloads"] = value; 203 | } 204 | } 205 | 206 | [global::System.Configuration.UserScopedSettingAttribute()] 207 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 208 | [global::System.Configuration.DefaultSettingValueAttribute("False")] 209 | public bool SaveThumbnails { 210 | get { 211 | return ((bool)(this["SaveThumbnails"])); 212 | } 213 | set { 214 | this["SaveThumbnails"] = value; 215 | } 216 | } 217 | 218 | [global::System.Configuration.UserScopedSettingAttribute()] 219 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 220 | [global::System.Configuration.DefaultSettingValueAttribute("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + 221 | "Chrome/125.0.0.0 Safari/537.36")] 222 | public string UserAgent { 223 | get { 224 | return ((string)(this["UserAgent"])); 225 | } 226 | set { 227 | this["UserAgent"] = value; 228 | } 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /GChan/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | C:\GChan 7 | 8 | 9 | 60000 10 | 11 | 12 | False 13 | 14 | 15 | True 16 | 17 | 18 | False 19 | 20 | 21 | True 22 | 23 | 24 | True 25 | 26 | 27 | True 28 | 29 | 30 | False 31 | 32 | 33 | 0 34 | 35 | 36 | 0 37 | 38 | 39 | False 40 | 41 | 42 | False 43 | 44 | 45 | True 46 | 47 | 48 | 35 49 | 50 | 51 | False 52 | 53 | 54 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 55 | 56 | 57 | -------------------------------------------------------------------------------- /GChan/System.Data.SQLite.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Issung/GChan/941daf0fa9e104768c0d7b4b635c889d96532dd4/GChan/System.Data.SQLite.dll -------------------------------------------------------------------------------- /GChan/TodoList.txt: -------------------------------------------------------------------------------- 1 | Todo items: 2 | Add (proper scaled) icons to all context menus. 3 | Archived thread indicator (with red text/highlight or seperate column (archived yes/no)). 4 | Asynchronous General.DownloadToDir downloads files as 0bytes empty files, fix this immediately. 5 | Make boards view a datagridview and make it use databinding. 6 | Add options to task bar right click 7 | Toggle Stop & Go (Allow users to pause timer while gaming, etc). 8 | Quick set interval with a little popup box. 9 | 10 | Maybes: 11 | Right click column headers -> choose visible columns. 12 | Add more column choices? 13 | Archived... 14 | 15 | Done items: 16 | Refactor code to seperate "Imageboard" into thread and board. 17 | 18 | Load threads and boards on program load on a seperate thread. 19 | Databinding on thread grid view. 20 | Ability to sort thread rows by clicking column headers. 21 | 22 | Added dropdown box (ComboBox) to settings to allow choice for saved files filename format. 23 | 24 | Copying all threads to clipboard also includes the custom subject text now. 25 | Copying URL to clipboard now includes the custom subject text in the URL as a parameter. 26 | 27 | Added new column displaying thread file count. 28 | 29 | Added option to add subject text to folder name once thread is archived or manually removed. 30 | 31 | Thread count in tab text. 32 | Changed Settings Page: 33 | Directory textbox is now readonly, opens directory on double click. 34 | Timer field is now a NumericUpDown. 35 | Custom subjects are now saved by appending it so saved URL as a GET URL parameter. 36 | Added disallowed characters to GetStringMessageBox. 37 | Removed annoying FocusCues thing on DataGridView by making custom class CustomDataGridView and overriding the bool. 38 | 39 | Changed icon. 40 | Add start with windows checkbox. 41 | Resize settings window. -------------------------------------------------------------------------------- /GChan/Trackers/Asset.cs: -------------------------------------------------------------------------------- 1 | using GChan.Controllers; 2 | using GChan.Helpers; 3 | using GChan.Properties; 4 | using GChan.Trackers; 5 | using NLog; 6 | using System; 7 | using System.IO; 8 | using System.Net; 9 | using CancellationToken = System.Threading.CancellationToken; 10 | 11 | namespace GChan 12 | { 13 | public class Asset : IDownloadable, IEquatable 14 | { 15 | /// 16 | /// For 4chan: Unix timestamp with microseconds at which the image was uploaded. 17 | /// 18 | public long Tim; 19 | 20 | /// 21 | /// URL to the access the image. 22 | /// 23 | public string Url; 24 | 25 | /// 26 | /// The sanitised filename the image was uploaded as without an extension.
27 | /// e.g. "LittleSaintJames", not the stored filename e.g. "1265123123.jpg". 28 | ///
29 | public string UploadedFilename; 30 | 31 | /// 32 | /// The ID of the post this image belongs to. 33 | /// 34 | public long No; 35 | 36 | /// 37 | /// The thread this image is from. 38 | /// 39 | public Thread Thread; 40 | 41 | private static readonly ILogger logger = LogManager.GetCurrentClassLogger(); 42 | 43 | public bool ShouldDownload => !Thread.Gone; 44 | 45 | public Asset( 46 | long tim, 47 | string url, 48 | string uploadedFilename, 49 | long no, 50 | Thread thread 51 | ) 52 | { 53 | Tim = tim; 54 | Url = url; 55 | UploadedFilename = Utils.SanitiseFilename(uploadedFilename); 56 | No = no; 57 | Thread = thread; 58 | } 59 | 60 | public void Download( 61 | DownloadManager.SuccessCallback successCallback, 62 | DownloadManager.FailureCallback failureCallback, 63 | CancellationToken cancellationToken 64 | ) 65 | { 66 | if (!ShouldDownload) 67 | { 68 | successCallback(this); 69 | return; 70 | } 71 | 72 | if (!Directory.Exists(Thread.SaveTo)) 73 | { 74 | Directory.CreateDirectory(Thread.SaveTo); 75 | } 76 | 77 | var destFilepath = Utils.CombinePathAndFilename(Thread.SaveTo, GenerateFilename((ImageFileNameFormat)Settings.Default.ImageFilenameFormat)); 78 | 79 | try 80 | { 81 | // TODO: Asyncify/Taskify. 82 | // TODO: Use cancellation token. 83 | using var webClient = Utils.CreateWebClient(); 84 | webClient.DownloadFile(Url, destFilepath); 85 | Thread.SavedIds.Add(Tim); 86 | successCallback(this); 87 | } 88 | catch (WebException webException) when (webException.IsGone(out var httpWebResponse)) 89 | { 90 | logger.Debug("Downloading {image_link} resulted in {status_code}", this, httpWebResponse.StatusCode); 91 | failureCallback(this, false); // Don't retry. 92 | } 93 | catch (Exception ex) 94 | { 95 | logger.Error(ex, "An error occured downloading an image."); 96 | failureCallback(this, true); 97 | } 98 | } 99 | 100 | public string GenerateFilename(ImageFileNameFormat format) 101 | { 102 | var extension = Path.GetExtension(Url); // Contains period (.). 103 | 104 | var result = format switch 105 | { 106 | ImageFileNameFormat.ID => $"{No}{extension}", 107 | ImageFileNameFormat.OriginalFilename => $"{UploadedFilename}{extension}", 108 | ImageFileNameFormat.IDAndOriginalFilename => $"{No} - {UploadedFilename}{extension}", 109 | ImageFileNameFormat.OriginalFilenameAndID => $"{UploadedFilename} - {No}{extension}", 110 | _ => throw new ArgumentException("Given value for 'format' is unknown.") 111 | }; 112 | 113 | return result; 114 | } 115 | 116 | public bool Equals(Asset other) 117 | { 118 | if (other == null) 119 | { 120 | return false; 121 | } 122 | 123 | return Tim == other.Tim && 124 | Url == other.Url && 125 | UploadedFilename == other.UploadedFilename; 126 | } 127 | 128 | public override bool Equals(object other) 129 | { 130 | if (other == null || GetType() != other.GetType()) 131 | { 132 | return false; 133 | } 134 | 135 | return this.Equals((Asset)other); 136 | } 137 | 138 | public override int GetHashCode() 139 | { 140 | unchecked 141 | { 142 | int hash = 17; 143 | hash = hash * 23 + Tim.GetHashCode(); 144 | hash = hash * 23 + (Url?.GetHashCode() ?? 0); 145 | hash = hash * 23 + (UploadedFilename?.GetHashCode() ?? 0); 146 | hash = hash * 23 + No.GetHashCode(); 147 | hash = hash * 23 + Thread.GetHashCode(); 148 | return hash; 149 | } 150 | } 151 | 152 | public override string ToString() 153 | { 154 | return $"ImageLink {{ Tim: '{Tim}', Url: '{Url}', UploadedFilename: '{UploadedFilename}', No: '{No}' }}"; 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /GChan/Trackers/Board.cs: -------------------------------------------------------------------------------- 1 | namespace GChan.Trackers 2 | { 3 | public abstract class Board : Tracker 4 | { 5 | protected int threadCount; 6 | 7 | public int ThreadCount 8 | { 9 | get 10 | { 11 | return threadCount; 12 | } 13 | } 14 | 15 | /// 16 | /// The greatest Thread ID added to tracking.
17 | /// This is used to ignore old thread ids in . 18 | ///
19 | public long GreatestThreadId { get; set; } 20 | 21 | protected Board(string url) : base(url) 22 | { 23 | Type = Type.Board; 24 | } 25 | 26 | public abstract string[] GetThreadLinks(); 27 | 28 | public override string ToString() 29 | { 30 | return $"{SiteName} - /{BoardCode}/ - ({ThreadCount} Threads)"; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /GChan/Trackers/IDownloadable.cs: -------------------------------------------------------------------------------- 1 | using GChan.Controllers; 2 | using System.Threading; 3 | 4 | namespace GChan 5 | { 6 | public interface IDownloadable where T : IDownloadable 7 | { 8 | /// 9 | /// Should this item be downloaded.
10 | /// Decision may have changed since being added to download manager. 11 | ///
12 | public bool ShouldDownload { get; } 13 | 14 | /// 15 | /// Perform download for this item. 16 | /// 17 | void Download( 18 | DownloadManager.SuccessCallback successCallback, 19 | DownloadManager.FailureCallback failureCallback, 20 | CancellationToken cancellationToken 21 | ); 22 | } 23 | } -------------------------------------------------------------------------------- /GChan/Trackers/Sites/Board_4Chan.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | using System.Windows.Forms; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Linq; 10 | 11 | namespace GChan.Trackers 12 | { 13 | public class Board_4Chan : Board 14 | { 15 | public const string SITE_NAME = "4chan"; 16 | public const string IS_BOARD_REGEX = "boards.(4chan|4channel).org/[a-zA-Z0-9]*?/*$"; 17 | public const string BOARD_CODE_REGEX = "(?<=((chan|channel).org/))[a-zA-Z0-9]+(?=(/))?"; 18 | 19 | public Board_4Chan(string URL) : base(URL) 20 | { 21 | SiteName = SITE_NAME; 22 | 23 | Match boardCodeMatch = Regex.Match(URL, BOARD_CODE_REGEX); 24 | BoardCode = boardCodeMatch.Groups[0].Value; 25 | } 26 | 27 | public static bool UrlIsBoard(string url) 28 | { 29 | return Regex.IsMatch(url, IS_BOARD_REGEX); 30 | } 31 | 32 | override public string[] GetThreadLinks() 33 | { 34 | //string URL = "http://a.4cdn.org/" + base.URL.Split('/')[3] + "/catalog.json"; //example: http://a.4cdn.org/b/catalog.json 35 | string URL = "http://a.4cdn.org/" + BoardCode + "/catalog.json"; 36 | List threadLinks = new List(); 37 | 38 | try 39 | { 40 | JArray jArray; 41 | using (var web = Utils.CreateWebClient()) 42 | { 43 | string json = web.DownloadString(URL); 44 | jArray = JArray.Parse(json); 45 | } 46 | 47 | threadLinks = jArray 48 | .SelectTokens("[*].threads[*]") 49 | .Select(x => "http://boards.4chan.org/" + BoardCode + "/thread/" + x["no"]) 50 | .ToList(); 51 | } 52 | catch (WebException webEx) 53 | { 54 | logger.Error(webEx, "Error occured attempting to get thread links."); 55 | 56 | #if DEBUG 57 | MessageBox.Show("Connection Error: " + webEx.Message); 58 | #endif 59 | } 60 | 61 | threadCount = threadLinks.Count; 62 | return threadLinks.ToArray(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /GChan/Trackers/Sites/Thread_4Chan.cs: -------------------------------------------------------------------------------- 1 | using GChan.Helpers; 2 | using GChan.Properties; 3 | using Newtonsoft.Json.Linq; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Net; 9 | using System.Text.RegularExpressions; 10 | 11 | namespace GChan.Trackers 12 | { 13 | public class Thread_4Chan : Thread 14 | { 15 | public const string THREAD_REGEX = "boards.(4chan|4channel).org/[a-zA-Z0-9]*?/thread/[0-9]*"; 16 | public const string BOARD_CODE_REGEX = "(?<=((chan|channel).org/))[a-zA-Z0-9]+(?=(/))?"; 17 | public const string ID_CODE_REGEX = "(?<=(thread/))[0-9]*(?=(.*))"; 18 | 19 | public Thread_4Chan(string url) : base(url) 20 | { 21 | SiteName = Board_4Chan.SITE_NAME; 22 | 23 | Match match = Regex.Match(url, @"boards.(4chan|4channel).org/[a-zA-Z0-9]*?/thread/\d*"); 24 | Url = "http://" + match.Groups[0].Value; 25 | 26 | Match boardCodeMatch = Regex.Match(url, BOARD_CODE_REGEX); 27 | BoardCode = boardCodeMatch.Groups[0].Value; 28 | 29 | Match idCodeMatch = Regex.Match(url, ID_CODE_REGEX); 30 | ID = idCodeMatch.Groups[0].Value; 31 | 32 | SaveTo = Path.Combine(Settings.Default.SavePath, SiteName, BoardCode, ID); 33 | 34 | if (subject == null) 35 | Subject = GetThreadSubject(); 36 | } 37 | 38 | public static bool UrlIsThread(string url) 39 | { 40 | return Regex.IsMatch(url, THREAD_REGEX); 41 | } 42 | 43 | protected override Asset[] GetImageLinksImpl(bool includeAlreadySaved = false) 44 | { 45 | var baseUrl = $"http://i.4cdn.org/{BoardCode}/"; 46 | var jsonUrl = $"http://a.4cdn.org/{BoardCode}/thread/{ID}.json"; 47 | 48 | using var web = Utils.CreateWebClient(); 49 | var json = web.DownloadString(jsonUrl); 50 | var jObject = JObject.Parse(json); 51 | 52 | // The /f/ board (flash) saves the files with their uploaded name. 53 | var timPath = BoardCode == "f" ? "filename" : "tim"; 54 | 55 | var links = jObject 56 | .SelectTokens("posts[*]") 57 | .Where(x => x["ext"] != null) 58 | .Select(x => 59 | new Asset( 60 | x[timPath].Value(), 61 | baseUrl + Uri.EscapeDataString(x[timPath].Value()) + x["ext"], // Require escaping for the flash files stored with arbitrary string names. 62 | x["filename"].Value(), 63 | x["no"].Value(), 64 | this 65 | ) 66 | ) 67 | .ToArray(); 68 | 69 | FileCount = links.Length; 70 | return links.MaybeRemoveAlreadySavedLinks(includeAlreadySaved, SavedIds).ToArray(); 71 | } 72 | 73 | public override void DownloadHtmlImpl() 74 | { 75 | var thumbUrls = new List(); 76 | var baseUrl = $"//i.4cdn.org/{BoardCode}/"; 77 | var jsonUrl = $"http://a.4cdn.org/{BoardCode}/thread/{ID}.json"; 78 | var htmlPage = string.Empty; 79 | JObject jObject; 80 | 81 | using (var web = Utils.CreateWebClient()) 82 | { 83 | htmlPage = web.DownloadString(Url); 84 | htmlPage = htmlPage.Replace("f=\"to\"", "f=\"penis\""); 85 | 86 | var json = web.DownloadString(jsonUrl); 87 | jObject = JObject.Parse(json); 88 | } 89 | 90 | var posts = jObject 91 | .SelectTokens("posts[*]") 92 | .Where(x => x["ext"] != null) 93 | .ToList(); 94 | 95 | foreach (var post in posts) 96 | { 97 | var tim = post["tim"].ToString(); 98 | var ext = post["ext"].ToString(); 99 | var oldUrl = baseUrl + tim + ext; 100 | var newFilename = Path.GetFileNameWithoutExtension( 101 | new Asset( 102 | post["tim"].Value(), 103 | oldUrl, 104 | post["filename"].ToString(), 105 | post["no"].Value(), 106 | this 107 | ).GenerateFilename((ImageFileNameFormat)Settings.Default.ImageFilenameFormat) 108 | ); 109 | 110 | htmlPage = htmlPage.Replace(oldUrl, tim + ext); 111 | 112 | if (ext == ".webm") 113 | { 114 | var thumbUrl = $"//t.4cdn.org/{BoardCode}/{tim}s.jpg"; 115 | thumbUrls.Add($"http:{thumbUrl}"); 116 | 117 | htmlPage = htmlPage.Replace(tim, newFilename); 118 | htmlPage = htmlPage.Replace($"{baseUrl}{newFilename}", $"thumb/{tim}"); 119 | } 120 | else 121 | { 122 | var thumbName = tim + "s"; 123 | htmlPage = htmlPage.Replace($"{thumbName}.jpg", tim + ext); 124 | htmlPage = htmlPage.Replace($"/{thumbName}", thumbName); 125 | 126 | htmlPage = htmlPage.Replace($"{baseUrl}{tim}", tim); 127 | htmlPage = htmlPage.Replace(tim, newFilename); 128 | } 129 | 130 | htmlPage = htmlPage.Replace($"//is2.4chan.org/{BoardCode}/{tim}", tim); 131 | htmlPage = htmlPage.Replace($"/{tim}{ext}", tim + ext); 132 | } 133 | 134 | // 4chan uses double slash urls (copy current protocol), when the user views it locally the protocol will no longer be http, so build it in. 135 | // This is used for javascript references. 136 | htmlPage = htmlPage.Replace("=\"//", "=\"http://"); 137 | 138 | // Alter all content links like "http://is2.4chan.org/tv/123.jpg" to become local like "123.jpg". 139 | htmlPage = htmlPage.Replace($"http://is2.4chan.org/{BoardCode}/", string.Empty); 140 | 141 | if (Settings.Default.SaveThumbnails) 142 | { 143 | foreach (var thumb in thumbUrls) 144 | { 145 | Utils.DownloadFileIfDoesntExist(thumb, $"{SaveTo}\\thumb"); 146 | } 147 | } 148 | 149 | if (!string.IsNullOrWhiteSpace(htmlPage)) 150 | { 151 | File.WriteAllText($"{SaveTo}\\Thread.html", htmlPage); 152 | } 153 | } 154 | 155 | 156 | protected override string GetThreadSubject() 157 | { 158 | string subject = NO_SUBJECT; 159 | 160 | try 161 | { 162 | string JSONUrl = "http://a.4cdn.org/" + BoardCode + "/thread/" + ID + ".json"; 163 | 164 | const string SUB_HEADER = "\"sub\":\""; 165 | const string SUB_ENDER = "\","; 166 | 167 | using (var web = Utils.CreateWebClient()) 168 | { 169 | string rawjson = web.DownloadString(JSONUrl); 170 | int subStartIndex = rawjson.IndexOf(SUB_HEADER); 171 | 172 | // If "Sub":" was found in json then there is a subject. 173 | if (subStartIndex >= 0) 174 | { 175 | //Increment along the rawjson until the ending ", sequence is found, then substring it to extract the subject. 176 | for (int i = subStartIndex; i < rawjson.Length; i++) 177 | { 178 | if (rawjson.Substring(i, SUB_ENDER.Length) == SUB_ENDER) 179 | { 180 | subject = rawjson.Substring(subStartIndex + SUB_HEADER.Length, i - (subStartIndex + SUB_HEADER.Length)); 181 | subject = Utils.SanitiseSubject(WebUtility.HtmlDecode(subject)); 182 | break; 183 | } 184 | } 185 | } 186 | } 187 | } 188 | catch 189 | { 190 | subject = NO_SUBJECT; 191 | } 192 | 193 | return subject; 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /GChan/Trackers/Thread.cs: -------------------------------------------------------------------------------- 1 | using GChan.Controllers; 2 | using GChan.Helpers; 3 | using GChan.Models; 4 | using System; 5 | using System.ComponentModel; 6 | using System.IO; 7 | using System.Net; 8 | using System.Runtime.CompilerServices; 9 | using CancellationToken = System.Threading.CancellationToken; 10 | 11 | namespace GChan.Trackers 12 | { 13 | /// 14 | /// implementation is for downloading the website HTML.
15 | /// For downloading images is used and results queued into a download manager. 16 | ///
17 | public abstract class Thread : Tracker, IDownloadable, INotifyPropertyChanged 18 | { 19 | public const string NO_SUBJECT = "No Subject"; 20 | 21 | public event PropertyChangedEventHandler PropertyChanged; 22 | 23 | public SavedIdsCollection SavedIds { get; set; } = new(); 24 | 25 | public bool ShouldDownload => !Gone; 26 | 27 | public string Subject 28 | { 29 | get => subject ?? NO_SUBJECT; 30 | set 31 | { 32 | subject = value; 33 | NotifyPropertyChanged(); 34 | } 35 | } 36 | 37 | /// 38 | /// The identifier of the thread (AKA No. (number)) 39 | /// 40 | public string ID { get; protected set; } 41 | 42 | public int? FileCount 43 | { 44 | get => fileCount; 45 | set 46 | { 47 | fileCount = value; 48 | 49 | if (!Program.mainForm.Disposing && !Program.mainForm.IsDisposed) 50 | { 51 | Program.mainForm.Invoke(() => { NotifyPropertyChanged(nameof(FileCount)); }); 52 | } 53 | } 54 | } 55 | 56 | public bool Gone { get; protected set; } = false; 57 | 58 | protected string subject { get; private set; } = null; 59 | private int? fileCount = null; 60 | 61 | protected Thread(string url) : base(url) 62 | { 63 | Type = Type.Thread; 64 | 65 | if (url.Contains("?")) 66 | { 67 | //TODO: Do this with Regex or Uri and HTTPUtility HttpUtility.ParseQueryString (https://stackoverflow.com/a/659929/8306962) 68 | subject = url.Substring(url.LastIndexOf('=') + 1).Replace('_', ' '); 69 | Url = url.Substring(0, url.LastIndexOf('/')); 70 | } 71 | } 72 | 73 | public void NotifyPropertyChanged([CallerMemberName] string propertyName = "") 74 | { 75 | #if DEBUG 76 | logger.Debug($"NotifyPropertyChanged! propertyName: {propertyName}."); 77 | #endif 78 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 79 | } 80 | 81 | public void Download( 82 | DownloadManager.SuccessCallback successCallback, 83 | DownloadManager.FailureCallback failureCallback, 84 | CancellationToken cancellationToken 85 | ) 86 | { 87 | if (!ShouldDownload) 88 | { 89 | successCallback(this); 90 | return; 91 | } 92 | 93 | try 94 | { 95 | // TODO: Forward cancellation token and use in each implementation. 96 | DownloadHtmlImpl(); 97 | } 98 | catch (WebException webEx) when (webEx.IsGone(out var httpWebResponse)) 99 | { 100 | Gone = true; 101 | } 102 | catch (Exception ex) 103 | { 104 | logger.Error(ex); 105 | failureCallback(this, true); 106 | } 107 | 108 | successCallback(this); 109 | } 110 | 111 | public abstract void DownloadHtmlImpl(); 112 | 113 | /// 114 | /// Get imagelinks for this thread. 115 | /// 116 | public Asset[] GetImageLinks() 117 | { 118 | if (Gone) 119 | { 120 | logger.Info($"Download(object callback) called on {this}, but will not download because {nameof(Gone)} is true."); 121 | return Array.Empty(); 122 | } 123 | 124 | try 125 | { 126 | if (!Directory.Exists(SaveTo)) 127 | { 128 | Directory.CreateDirectory(SaveTo); 129 | } 130 | 131 | var imageLinks = GetImageLinksImpl(); 132 | return imageLinks; 133 | } 134 | catch (WebException webEx) when (webEx.IsGone(out var httpWebResponse)) 135 | { 136 | Gone = true; 137 | } 138 | catch (Exception ex) 139 | { 140 | logger.Error(ex); 141 | } 142 | 143 | return Array.Empty(); 144 | } 145 | 146 | /// 147 | /// Implementation point for website specific image link retreival. 148 | /// 149 | protected abstract Asset[] GetImageLinksImpl(bool includeAlreadySaved = false); 150 | 151 | protected abstract string GetThreadSubject(); 152 | 153 | public string GetURLWithSubject() 154 | { 155 | return (Url + ("/?subject=" + Utils.SanitiseSubject(Subject).Replace(' ', '_'))).Replace("\r", ""); 156 | } 157 | 158 | public override int GetHashCode() 159 | { 160 | unchecked 161 | { 162 | int hash = 3; 163 | hash = hash * 13 + SiteName.GetHashCode(); 164 | hash = hash * 13 + BoardCode.GetHashCode(); 165 | hash = hash * 13 + ID.GetHashCode(); 166 | return hash; 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /GChan/Trackers/Tracker.cs: -------------------------------------------------------------------------------- 1 | using NLog; 2 | using System.Net; 3 | 4 | namespace GChan.Trackers 5 | { 6 | public enum Type { Board, Thread }; 7 | 8 | public abstract class Tracker 9 | { 10 | public string Url { get; protected set; } 11 | 12 | public string SaveTo { get; protected set; } 13 | 14 | public Type Type { get; protected set; } 15 | 16 | public string SiteName { get; protected set; } 17 | 18 | /// 19 | /// Code for the board this is tracking, excluding slashes. 20 | /// e.g. gif, r9k, b 21 | /// 22 | public string BoardCode { get; protected set; } 23 | 24 | /// 25 | /// Whether or not to keep scraping this tracker. 26 | /// 27 | public bool Scraping { get; set; } = true; 28 | 29 | /// 30 | /// Response status codes that indicate content is no longer available. 31 | /// 32 | public static readonly HttpStatusCode?[] GoneStatusCodes = 33 | { 34 | HttpStatusCode.NotFound, 35 | HttpStatusCode.Gone, 36 | }; 37 | 38 | protected readonly ILogger logger = LogManager.GetCurrentClassLogger(); 39 | 40 | protected Tracker(string url) 41 | { 42 | Url = url; 43 | } 44 | 45 | public override string ToString() 46 | { 47 | if (this is Thread thread) 48 | { 49 | return $"Thread {{ {SiteName}, /{BoardCode}/, {thread.ID}, Gone: {thread.Gone} }}"; 50 | } 51 | else if (this is Board) 52 | { 53 | return $"Board {{ {SiteName}, /{BoardCode}/ }}"; 54 | } 55 | else 56 | { 57 | return $"{this.GetType().Name} {{ {SiteName}, /{BoardCode}/ }}"; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /GChan/icons/alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Issung/GChan/941daf0fa9e104768c0d7b4b635c889d96532dd4/GChan/icons/alert.png -------------------------------------------------------------------------------- /GChan/icons/clipboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Issung/GChan/941daf0fa9e104768c0d7b4b635c889d96532dd4/GChan/icons/clipboard.png -------------------------------------------------------------------------------- /GChan/icons/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Issung/GChan/941daf0fa9e104768c0d7b4b635c889d96532dd4/GChan/icons/close.png -------------------------------------------------------------------------------- /GChan/icons/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Issung/GChan/941daf0fa9e104768c0d7b4b635c889d96532dd4/GChan/icons/download.png -------------------------------------------------------------------------------- /GChan/icons/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Issung/GChan/941daf0fa9e104768c0d7b4b635c889d96532dd4/GChan/icons/file.png -------------------------------------------------------------------------------- /GChan/icons/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Issung/GChan/941daf0fa9e104768c0d7b4b635c889d96532dd4/GChan/icons/folder.png -------------------------------------------------------------------------------- /GChan/icons/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Issung/GChan/941daf0fa9e104768c0d7b4b635c889d96532dd4/GChan/icons/question.png -------------------------------------------------------------------------------- /GChan/icons/rename.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Issung/GChan/941daf0fa9e104768c0d7b4b635c889d96532dd4/GChan/icons/rename.png -------------------------------------------------------------------------------- /GChan/icons/settings-wrench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Issung/GChan/941daf0fa9e104768c0d7b4b635c889d96532dd4/GChan/icons/settings-wrench.png -------------------------------------------------------------------------------- /GChan/icons/world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Issung/GChan/941daf0fa9e104768c0d7b4b635c889d96532dd4/GChan/icons/world.png -------------------------------------------------------------------------------- /GChan/nlog.config: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GChan 2 | [![Release](https://img.shields.io/github/v/release/issung/GChan?style=for-the-badge)](https://github.com/Issung/GChan/releases) 3 | [![Downloads](https://img.shields.io/github/downloads/issung/GCHan/total?style=for-the-badge)](https://github.com/Issung/GChan/releases) 4 | [![Discord](https://img.shields.io/discord/806067523853746196?color=%23738BD8&label=discord&style=for-the-badge)](https://discord.gg/Ayh6UasAVn) 5 | 6 | GChan is an automatic imageboard scraper, it supports multithreading and auto-rechecking of threads with a custom set timer. 7 | 8 | GChan is a fork of YChan which aims to improve stability and add extra features which some may find useful. 9 | 10 | #### Project Status: Active Development + Bug Maintenance. 11 | 12 | ### Supported Websites: 13 | * [4chan.org](http://4chan.org/) 14 | 15 | Requires .NET Framework 4.8 or higher, made with Windows Forms. 16 | 17 | ![GChan Window](http://puu.sh/ERKQ8.png) 18 | 19 | ## Contributing 20 | When contributing please: 21 | 1. First make an issue to discuss the change or if it is an existing issue please ask on the issue to be assigned the problem, someone may already be working on it. 22 | 2. On your fork's branch please prefix the branch with `feature/` or `bugfix/` and the use the issue number. E.g. `feature/#5-support-2chan`. 23 | 3. Show proof of testing in your pull request, or have created tests in the code. 24 | 4. Please keep the changes restricted to one area, make the purpose of the change easy to locate and easy to understand. 25 | 26 | ## Features 27 | * Intuitive thread layout using grid. 28 | * Adjustable columns display. 29 | * Subject renaming. 30 | * Sorting. 31 | * Ease of adding new threads/boards. 32 | * Add multiple threads at once seperated by a comma (,). 33 | * Click Add button without pasting it into the text box to add URL from clipboard. 34 | * Drag and drop URL into window to add it. 35 | * Right click a thread and copy URL to clipboard. 36 | * Copy all thread URLs to clipboard seperated by a comma (,). 37 | * Display of tracked threads and board count on tabs. 38 | * Many settings: 39 | * Optionally start with Windows, optionally hidden. 40 | * Options to add a thread's subject to thread's directory when a thread is removed. 41 | * Name format choice of 'ID - Subject' or 'Subject - ID'. 42 | * Choices for saved files filename format: 43 | * ID Only (e.g. '1570301.jpg') 44 | * Original Filename Only (e.g. 'LittleSaintJames.jpg') 45 | * ID - OriginalFilename (e.g. '1570301 - LittleSaintJames.jpg') 46 | * OriginalFilename - ID (e.g. 'LittleSaintJames - 1570301.jpg') 47 | -------------------------------------------------------------------------------- /packageRelease.ps1: -------------------------------------------------------------------------------- 1 | # Define paths 2 | $outputPath = ".\" 3 | $tempPath = "$env:TEMP\GChanReleaseBuild" 4 | $zipNameTemplate = "GChan{0}.zip" 5 | $folderNameTemplate = "GChan{0}" 6 | 7 | # Clean up previous temp directory if it exists 8 | if (Test-Path $tempPath) { 9 | Remove-Item -Recurse -Force $tempPath 10 | } 11 | 12 | # Create new temp directory 13 | New-Item -ItemType Directory -Force -Path $tempPath 14 | 15 | # Get the assembly version 16 | $assemblyInfoPath = ".\GChan\Properties\AssemblyInfo.cs" 17 | $assemblyVersion = Select-String -Pattern 'AssemblyVersion\("([0-9\.]+)"\)' -Path $assemblyInfoPath | ForEach-Object { $_.Matches.Groups[1].Value } 18 | 19 | if (-not $assemblyVersion) { 20 | Write-Error "Assembly version not found in AssemblyInfo.cs" 21 | exit 1 22 | } 23 | 24 | # Build the release configuration using msbuild 25 | & msbuild GChan /p:Configuration=Release /p:OutputPath=$tempPath 26 | 27 | if ($LASTEXITCODE -ne 0) { 28 | Write-Error "Build failed" 29 | exit 1 30 | } 31 | 32 | # Create a new directory inside tempPath with the versioned folder name 33 | $versionedFolderName = [string]::Format($folderNameTemplate, $assemblyVersion) 34 | $versionedFolderPath = Join-Path -Path $tempPath -ChildPath $versionedFolderName 35 | New-Item -ItemType Directory -Force -Path $versionedFolderPath 36 | 37 | # Move all build output to the versioned folder 38 | Get-ChildItem -Path $tempPath -Exclude $versionedFolderName | Move-Item -Destination $versionedFolderPath 39 | 40 | # Create the zip file 41 | $zipFileName = [string]::Format($zipNameTemplate, $assemblyVersion) 42 | $zipFilePath = Join-Path -Path $outputPath -ChildPath $zipFileName 43 | 44 | Add-Type -AssemblyName System.IO.Compression.FileSystem 45 | [System.IO.Compression.ZipFile]::CreateFromDirectory($tempPath, $zipFilePath) 46 | 47 | Write-Host "Build and packaging complete: $zipFilePath" 48 | --------------------------------------------------------------------------------