├── .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 | [](https://github.com/Issung/GChan/releases)
3 | [](https://github.com/Issung/GChan/releases)
4 | [](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 | 
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 |
--------------------------------------------------------------------------------