├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── build.yml │ ├── code_analysis.yml │ ├── codeql.yml │ ├── release.yml │ └── sync_to_gitee.yml ├── .gitignore ├── Get_it_from_Microsoft_Badge.png ├── LICENSE.txt ├── README.md ├── XoW.sln ├── XoW.sln.DotSettings ├── XoW ├── App.xaml ├── App.xaml.cs ├── AppConfig.template.xml ├── Assets │ ├── BadgeLogo.scale-100.png │ ├── BadgeLogo.scale-125.png │ ├── BadgeLogo.scale-150.png │ ├── BadgeLogo.scale-200.png │ ├── BadgeLogo.scale-400.png │ ├── LargeTile.scale-100.png │ ├── LargeTile.scale-125.png │ ├── LargeTile.scale-150.png │ ├── LargeTile.scale-200.png │ ├── LargeTile.scale-400.png │ ├── SmallTile.scale-100.png │ ├── SmallTile.scale-125.png │ ├── SmallTile.scale-150.png │ ├── SmallTile.scale-200.png │ ├── SmallTile.scale-400.png │ ├── SplashScreen.scale-100.png │ ├── SplashScreen.scale-125.png │ ├── SplashScreen.scale-150.png │ ├── SplashScreen.scale-200.png │ ├── SplashScreen.scale-400.png │ ├── Square150x150Logo.scale-100.png │ ├── Square150x150Logo.scale-125.png │ ├── Square150x150Logo.scale-150.png │ ├── Square150x150Logo.scale-200.png │ ├── Square150x150Logo.scale-400.png │ ├── Square44x44Logo.altform-lightunplated_targetsize-16.png │ ├── Square44x44Logo.altform-lightunplated_targetsize-24.png │ ├── Square44x44Logo.altform-lightunplated_targetsize-256.png │ ├── Square44x44Logo.altform-lightunplated_targetsize-32.png │ ├── Square44x44Logo.altform-lightunplated_targetsize-48.png │ ├── Square44x44Logo.altform-unplated_targetsize-16.png │ ├── Square44x44Logo.altform-unplated_targetsize-24.png │ ├── Square44x44Logo.altform-unplated_targetsize-256.png │ ├── Square44x44Logo.altform-unplated_targetsize-32.png │ ├── Square44x44Logo.altform-unplated_targetsize-48.png │ ├── Square44x44Logo.scale-100.png │ ├── Square44x44Logo.scale-125.png │ ├── Square44x44Logo.scale-150.png │ ├── Square44x44Logo.scale-200.png │ ├── Square44x44Logo.scale-400.png │ ├── Square44x44Logo.targetsize-16.png │ ├── Square44x44Logo.targetsize-24.png │ ├── Square44x44Logo.targetsize-256.png │ ├── Square44x44Logo.targetsize-32.png │ ├── Square44x44Logo.targetsize-48.png │ ├── StoreLogo.backup.png │ ├── StoreLogo.scale-100.png │ ├── StoreLogo.scale-125.png │ ├── StoreLogo.scale-150.png │ ├── StoreLogo.scale-200.png │ ├── StoreLogo.scale-400.png │ ├── Wide310x150Logo.scale-100.png │ ├── Wide310x150Logo.scale-125.png │ ├── Wide310x150Logo.scale-150.png │ ├── Wide310x150Logo.scale-200.png │ └── Wide310x150Logo.scale-400.png ├── Constants.cs ├── Exceptions.cs ├── GlobalState.cs ├── Models │ ├── AiFaDianSponsor.cs │ ├── AnoBbsCookie.cs │ ├── CdnUrl.cs │ ├── ForumGroup.cs │ ├── ForumThread.cs │ ├── IncrementalLoadingSources.cs │ ├── ObservableObject.cs │ ├── ThreadDataContext.cs │ ├── ThreadReply.cs │ └── ThreadSubscription.cs ├── Package.appxmanifest ├── Properties │ ├── AssemblyInfo.cs │ └── Default.rd.xml ├── Services │ ├── AiFaDianApiClient.cs │ ├── AnoBbsApiClient.cs │ ├── ConfigurationManager.cs │ ├── Converter.cs │ ├── HtmlParser.cs │ ├── HttpClientService.cs │ └── QrCodeService.cs ├── Utils │ ├── ApplicationConfigurationHelper.cs │ ├── CommonUtils.cs │ ├── ComponentsBuilder.cs │ └── Extensions.cs ├── Views │ ├── ConfirmationContentDialog.xaml │ ├── ConfirmationContentDialog.xaml.cs │ ├── ContentDialogWithInput.xaml │ ├── ContentDialogWithInput.xaml.cs │ ├── LargeImageViewUserControl.xaml │ ├── LargeImageViewUserControl.xaml.cs │ ├── MainPage.xaml │ ├── MainPage.xaml.cs │ ├── MainPageHelper.cs │ ├── NotificationContentDialog.xaml │ ├── NotificationContentDialog.xaml.cs │ ├── ReportThreadContentDialog.xaml │ ├── ReportThreadContentDialog.xaml.cs │ ├── SettingsPage.xaml │ └── SettingsPage.xaml.cs └── XoW.csproj ├── azure-pipelines.yml └── nuget.config /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | # Remove the line below if you want to inherit .editorconfig settings from higher directories 3 | 4 | # C# files 5 | [*.cs] 6 | 7 | #### Core EditorConfig Options #### 8 | 9 | # Indentation and spacing 10 | indent_style = space 11 | 12 | # New line preferences 13 | insert_final_newline = true 14 | 15 | #### .NET Coding Conventions #### 16 | 17 | # Organize usings 18 | dotnet_separate_import_directive_groups = false 19 | dotnet_sort_system_directives_first = true 20 | 21 | # Expression-level preferences 22 | dotnet_style_namespace_match_folder = true 23 | 24 | # Suppression preferences 25 | dotnet_remove_unnecessary_suppression_exclusions = none 26 | 27 | # New line preferences 28 | dotnet_style_allow_multiple_blank_lines_experimental = false 29 | dotnet_style_allow_statement_immediately_after_block_experimental = false 30 | 31 | #### C# Coding Conventions #### 32 | 33 | # var preferences 34 | csharp_style_var_elsewhere = true:silent 35 | csharp_style_var_for_built_in_types = true:silent 36 | csharp_style_var_when_type_is_apparent = true:silent 37 | 38 | # Expression-bodied members 39 | csharp_style_expression_bodied_accessors = true:silent 40 | csharp_style_expression_bodied_constructors = false:silent 41 | csharp_style_expression_bodied_indexers = true:silent 42 | csharp_style_expression_bodied_lambdas = true:silent 43 | csharp_style_expression_bodied_local_functions = true:silent 44 | csharp_style_expression_bodied_methods = true:silent 45 | csharp_style_expression_bodied_operators = true:silent 46 | csharp_style_expression_bodied_properties = true:silent 47 | 48 | # Pattern matching preferences 49 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 50 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 51 | csharp_style_prefer_extended_property_pattern = true:suggestion 52 | csharp_style_prefer_not_pattern = true:suggestion 53 | csharp_style_prefer_pattern_matching = true:silent 54 | csharp_style_prefer_switch_expression = true:suggestion 55 | 56 | # Null-checking preferences 57 | csharp_style_conditional_delegate_call = true:suggestion 58 | csharp_style_prefer_parameter_null_checking = true:suggestion 59 | 60 | # Modifier preferences 61 | csharp_prefer_static_local_function = true:suggestion 62 | csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async 63 | 64 | # Code-block preferences 65 | csharp_prefer_braces = true:silent 66 | csharp_prefer_simple_using_statement = true:suggestion 67 | csharp_style_namespace_declarations = block_scoped:silent 68 | csharp_style_prefer_method_group_conversion = true:silent 69 | 70 | # Expression-level preferences 71 | csharp_prefer_simple_default_expression = true:suggestion 72 | csharp_style_deconstructed_variable_declaration = true:suggestion 73 | csharp_style_implicit_object_creation_when_type_is_apparent = false:suggestion 74 | csharp_style_inlined_variable_declaration = true:suggestion 75 | csharp_style_prefer_index_operator = false:suggestion 76 | csharp_style_prefer_local_over_anonymous_function = true:suggestion 77 | csharp_style_prefer_null_check_over_type_check = true:suggestion 78 | csharp_style_prefer_range_operator = false:suggestion 79 | csharp_style_prefer_tuple_swap = true:suggestion 80 | csharp_style_throw_expression = true:suggestion 81 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 82 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent 83 | 84 | # 'using' directive preferences 85 | csharp_using_directive_placement = outside_namespace:silent 86 | 87 | # New line preferences 88 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent 89 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent 90 | csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent 91 | 92 | #### C# Formatting Rules #### 93 | 94 | # New line preferences 95 | csharp_new_line_before_catch = true 96 | csharp_new_line_before_else = true 97 | csharp_new_line_before_finally = true 98 | csharp_new_line_before_members_in_anonymous_types = true 99 | csharp_new_line_before_members_in_object_initializers = true 100 | csharp_new_line_before_open_brace = all 101 | csharp_new_line_between_query_expression_clauses = true 102 | 103 | # Indentation preferences 104 | csharp_indent_block_contents = true 105 | csharp_indent_braces = false 106 | csharp_indent_case_contents = true 107 | csharp_indent_case_contents_when_block = false 108 | csharp_indent_labels = one_less_than_current 109 | csharp_indent_switch_labels = true 110 | 111 | # Space preferences 112 | csharp_space_after_cast = false 113 | csharp_space_after_colon_in_inheritance_clause = true 114 | csharp_space_after_comma = true 115 | csharp_space_after_dot = false 116 | csharp_space_after_keywords_in_control_flow_statements = true 117 | csharp_space_after_semicolon_in_for_statement = true 118 | csharp_space_around_binary_operators = before_and_after 119 | csharp_space_around_declaration_statements = false 120 | csharp_space_before_colon_in_inheritance_clause = true 121 | csharp_space_before_comma = false 122 | csharp_space_before_dot = false 123 | csharp_space_before_open_square_brackets = false 124 | csharp_space_before_semicolon_in_for_statement = false 125 | csharp_space_between_empty_square_brackets = false 126 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 127 | csharp_space_between_method_call_name_and_opening_parenthesis = false 128 | csharp_space_between_method_call_parameter_list_parentheses = false 129 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 130 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 131 | csharp_space_between_method_declaration_parameter_list_parentheses = false 132 | csharp_space_between_parentheses = false 133 | csharp_space_between_square_brackets = false 134 | 135 | # Wrapping preferences 136 | csharp_preserve_single_line_blocks = true 137 | csharp_preserve_single_line_statements = false 138 | 139 | #### Naming styles #### 140 | 141 | # Naming rules 142 | 143 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 144 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 145 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 146 | 147 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 148 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 149 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 150 | 151 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 152 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 153 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 154 | 155 | # Symbol specifications 156 | 157 | dotnet_naming_symbols.interface.applicable_kinds = interface 158 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 159 | dotnet_naming_symbols.interface.required_modifiers = 160 | 161 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 162 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 163 | dotnet_naming_symbols.types.required_modifiers = 164 | 165 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 166 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 167 | dotnet_naming_symbols.non_field_members.required_modifiers = 168 | 169 | # Naming styles 170 | 171 | dotnet_naming_style.pascal_case.required_prefix = 172 | dotnet_naming_style.pascal_case.required_suffix = 173 | dotnet_naming_style.pascal_case.word_separator = 174 | dotnet_naming_style.pascal_case.capitalization = pascal_case 175 | 176 | dotnet_naming_style.begins_with_i.required_prefix = I 177 | dotnet_naming_style.begins_with_i.required_suffix = 178 | dotnet_naming_style.begins_with_i.word_separator = 179 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 180 | 181 | # ReSharper properties 182 | resharper_csharp_blank_lines_inside_region = 0 183 | resharper_csharp_wrap_after_invocation_lpar = false 184 | resharper_csharp_wrap_arguments_style = chop_if_long 185 | resharper_csharp_wrap_before_invocation_rpar = false 186 | resharper_csharp_wrap_lines = false 187 | resharper_csharp_wrap_parameters_style = chop_if_long 188 | resharper_csharp_wrap_ternary_expr_style = chop_always 189 | resharper_force_chop_compound_if_expression = false 190 | resharper_keep_existing_expr_member_arrangement = false 191 | resharper_keep_existing_invocation_parens_arrangement = false 192 | resharper_max_invocation_arguments_on_line = 5 193 | resharper_place_simple_anonymousmethod_on_single_line = false 194 | resharper_place_simple_initializer_on_single_line = true 195 | resharper_trailing_comma_in_multiline_lists = false 196 | resharper_wrap_after_invocation_lpar = false 197 | resharper_wrap_before_invocation_rpar = false 198 | resharper_wrap_chained_method_calls = chop_if_long 199 | resharper_wrap_object_and_collection_initializer_style = chop_always 200 | 201 | [*.{cs,vb}] 202 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 203 | tab_width = 4 204 | indent_size = 4 205 | end_of_line = crlf 206 | dotnet_style_coalesce_expression = true:suggestion 207 | dotnet_style_null_propagation = true:suggestion 208 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 209 | dotnet_style_prefer_auto_properties = true:silent 210 | dotnet_style_object_initializer = true:suggestion 211 | dotnet_style_collection_initializer = true:suggestion 212 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 213 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 214 | dotnet_style_prefer_conditional_expression_over_return = true:silent 215 | dotnet_style_explicit_tuple_names = true:suggestion 216 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 217 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 218 | dotnet_style_prefer_compound_assignment = true:suggestion 219 | dotnet_style_prefer_simplified_interpolation = true:suggestion 220 | dotnet_style_namespace_match_folder = true:suggestion 221 | dotnet_style_readonly_field = true:suggestion 222 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 223 | dotnet_style_predefined_type_for_member_access = true:silent 224 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 225 | dotnet_style_allow_multiple_blank_lines_experimental = false:silent 226 | dotnet_style_allow_statement_immediately_after_block_experimental = false:silent 227 | dotnet_code_quality_unused_parameters = all:suggestion 228 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 229 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 230 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 231 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 232 | dotnet_style_qualification_for_field = false:silent 233 | dotnet_style_qualification_for_property = false:silent 234 | dotnet_style_qualification_for_method = false:silent 235 | dotnet_style_qualification_for_event = false:silent 236 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: 14 | - https://afdian.net/a/xdao4windows 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | 8 | - package-ecosystem: nuget 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build packages 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | paths: [ 'XoW.sln', 'XoW/**', '.github/workflows/build.yml' ] 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | name: Build packages 12 | runs-on: windows-latest 13 | env: 14 | APP_PACKAGES_DIRECTORY: $pwd\AppPackages 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install signing certificate 19 | shell: pwsh 20 | env: 21 | PFX_CONTENT: ${{ secrets.SIGNING_CERTIFICATE }} 22 | SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD }} 23 | run: | 24 | echo "Saving the certificate to $($env:RUNNER_TEMP)"; 25 | $pfxPath = Join-Path -Path $env:RUNNER_TEMP -ChildPath "cert.pfx"; 26 | $encodedBytes = [System.Convert]::FromBase64String($env:PFX_CONTENT); 27 | Set-Content $pfxPath -Value $encodedBytes -AsByteStream; 28 | echo "Installing certificate from $($env:RUNNER_TEMP)\cert.pfx"; 29 | Import-PfxCertificate -FilePath "$($env:RUNNER_TEMP)\cert.pfx" -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:SIGNING_CERTIFICATE_PASSWORD -Force -AsPlainText); 30 | 31 | - name: Add AppConfig.xml to working directory 32 | shell: pwsh 33 | env: 34 | APP_CONFIG_XML: ${{ secrets.APP_CONFIG_XML }} 35 | run: | 36 | echo "Adding AppConfig.xml to $($env:GITHUB_WORKSPACE)\XoW"; 37 | $filePath = Join-Path -Path "$($env:GITHUB_WORKSPACE)\XoW" -ChildPath "AppConfig.xml"; 38 | $encodedBytes = [System.Convert]::FromBase64String($env:APP_CONFIG_XML); 39 | Set-Content $filePath -Value $encodedBytes -AsByteStream; 40 | 41 | - name: Setup MSBuild 42 | uses: microsoft/setup-msbuild@v1 43 | 44 | - name: Build packages 45 | shell: pwsh 46 | run: >- 47 | msbuild 48 | "XoW.sln" 49 | /restore 50 | /m 51 | /verbosity:minimal 52 | /target:Rebuild 53 | /nodeReuse:false 54 | /p:AppxBundle=Always 55 | /p:AppxPackageDir="${{ env.APP_PACKAGES_DIRECTORY }}" 56 | /p:UapAppxPackageBuildMode=StoreAndSideload 57 | /p:AppxBundlePlatforms="x86|x64|ARM|ARM64" 58 | /p:configuration="Release" 59 | /p:VisualStudioVersion="17.0" 60 | 61 | - name: Load package version 62 | shell: pwsh 63 | run: | 64 | [xml]$manifest = Get-Content "$pwd\XoW\Package.appxmanifest" 65 | echo "VERSION=$($manifest.Package.Identity.Version)" >> $env:GITHUB_ENV 66 | 67 | - name: Upload workflow artifacts 68 | uses: actions/upload-artifact@v3 69 | with: 70 | name: Packages 71 | path: | 72 | AppPackages\XoW_${{ env.VERSION }}_Test\XoW_${{ env.VERSION }}_x86_x64_arm_arm64.msixbundle 73 | AppPackages\XoW_${{ env.VERSION }}_x86_x64_arm_arm64_bundle.msixupload 74 | -------------------------------------------------------------------------------- /.github/workflows/code_analysis.yml: -------------------------------------------------------------------------------- 1 | name: Code analysis 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | paths: [ 'XoW.sln', 'XoW/**', '.github/workflows/code_analysis.yml' ] 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | name: Code analysis 12 | runs-on: windows-latest 13 | steps: 14 | - name: Set up JDK 15 | uses: actions/setup-java@v3 16 | with: 17 | distribution: 'liberica' 18 | java-version: 17 19 | 20 | - uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 23 | 24 | - name: Cache SonarCloud packages 25 | uses: actions/cache@v3 26 | with: 27 | path: ~\sonar\cache 28 | key: ${{ runner.os }}-sonar 29 | restore-keys: ${{ runner.os }}-sonar 30 | 31 | - name: Cache SonarCloud scanner 32 | id: cache-sonar-scanner 33 | uses: actions/cache@v3 34 | with: 35 | path: .\.sonar\scanner 36 | key: ${{ runner.os }}-sonar-scanner 37 | restore-keys: ${{ runner.os }}-sonar-scanner 38 | 39 | - name: Install SonarCloud scanner 40 | if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' 41 | shell: powershell 42 | run: | 43 | New-Item -Path .\.sonar\scanner -ItemType Directory 44 | dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner 45 | 46 | - name: Install signing certificate 47 | shell: pwsh 48 | env: 49 | PFX_CONTENT: ${{ secrets.SIGNING_CERTIFICATE }} 50 | SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD }} 51 | run: | 52 | echo "Saving the certificate to $($env:RUNNER_TEMP)"; 53 | $pfxPath = Join-Path -Path $env:RUNNER_TEMP -ChildPath "cert.pfx"; 54 | $encodedBytes = [System.Convert]::FromBase64String($env:PFX_CONTENT); 55 | Set-Content $pfxPath -Value $encodedBytes -AsByteStream; 56 | echo "Installing certificate from $($env:RUNNER_TEMP)\cert.pfx"; 57 | Import-PfxCertificate -FilePath "$($env:RUNNER_TEMP)\cert.pfx" -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:SIGNING_CERTIFICATE_PASSWORD -Force -AsPlainText); 58 | 59 | - name: Add AppConfig.xml to working directory 60 | shell: pwsh 61 | env: 62 | APP_CONFIG_XML: ${{ secrets.APP_CONFIG_XML }} 63 | run: | 64 | echo "Adding AppConfig.xml to $($env:GITHUB_WORKSPACE)\XoW"; 65 | $filePath = Join-Path -Path "$($env:GITHUB_WORKSPACE)\XoW" -ChildPath "AppConfig.xml"; 66 | $encodedBytes = [System.Convert]::FromBase64String($env:APP_CONFIG_XML); 67 | Set-Content $filePath -Value $encodedBytes -AsByteStream; 68 | 69 | - name: Setup MSBuild 70 | uses: microsoft/setup-msbuild@v1 71 | 72 | - name: Build and analyze 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 75 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 76 | shell: powershell 77 | run: | 78 | .\.sonar\scanner\dotnet-sonarscanner begin /k:"boris1993_XoW" /o:"boris1993-github" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" 79 | msbuild "XoW.sln" /restore /m /t:Rebuild /nr:false /verbosity:minimal /p:AppxBundle=Always /p:UapAppxPackageBuildMode=StoreUpload /p:AppxBundlePlatforms="x86|x64|ARM|ARM64" /p:configuration="Debug" /p:VisualStudioVersion="17.0" 80 | .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" 81 | 82 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | paths: [ 'XoW.sln', 'XoW/**', '.github/workflows/codeql.yml' ] 18 | pull_request: 19 | # The branches below must be a subset of the branches above 20 | branches: [ "master" ] 21 | paths: [ 'XoW.sln', 'XoW/**', '.github/workflows/codeql.yml' ] 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | # Runner size impacts CodeQL analysis time. To learn more, please see: 27 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 28 | # - https://gh.io/supported-runners-and-hardware-resources 29 | # - https://gh.io/using-larger-runners 30 | # Consider using larger runners for possible analysis time improvements. 31 | runs-on: windows-latest 32 | permissions: 33 | actions: read 34 | contents: read 35 | security-events: write 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v4 40 | 41 | # Initializes the CodeQL tools for scanning. 42 | - name: Initialize CodeQL 43 | uses: github/codeql-action/init@v2 44 | with: 45 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] 46 | # Use only 'java' to analyze code written in Java, Kotlin or both 47 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 48 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 49 | languages: 'csharp' 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | - name: Install signing certificate 58 | shell: pwsh 59 | env: 60 | PFX_CONTENT: ${{ secrets.SIGNING_CERTIFICATE }} 61 | SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD }} 62 | run: | 63 | echo "Saving the certificate to $($env:RUNNER_TEMP)"; 64 | $pfxPath = Join-Path -Path $env:RUNNER_TEMP -ChildPath "cert.pfx"; 65 | $encodedBytes = [System.Convert]::FromBase64String($env:PFX_CONTENT); 66 | Set-Content $pfxPath -Value $encodedBytes -AsByteStream; 67 | echo "Installing certificate from $($env:RUNNER_TEMP)\cert.pfx"; 68 | Import-PfxCertificate -FilePath "$($env:RUNNER_TEMP)\cert.pfx" -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:SIGNING_CERTIFICATE_PASSWORD -Force -AsPlainText); 69 | 70 | - name: Add AppConfig.xml to working directory 71 | shell: pwsh 72 | env: 73 | APP_CONFIG_XML: ${{ secrets.APP_CONFIG_XML }} 74 | run: | 75 | echo "Adding AppConfig.xml to $($env:GITHUB_WORKSPACE)\XoW"; 76 | $filePath = Join-Path -Path "$($env:GITHUB_WORKSPACE)\XoW" -ChildPath "AppConfig.xml"; 77 | $encodedBytes = [System.Convert]::FromBase64String($env:APP_CONFIG_XML); 78 | Set-Content $filePath -Value $encodedBytes -AsByteStream; 79 | 80 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 81 | # If this step fails, then you should remove it and run the build manually (see below) 82 | # - name: Autobuild 83 | # uses: github/codeql-action/autobuild@v2 84 | 85 | # ℹ️ Command-line programs to run using the OS shell. 86 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 87 | 88 | # If the Autobuild fails above, remove it and uncomment the following three lines. 89 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 90 | 91 | - name: Setup MSBuild 92 | uses: microsoft/setup-msbuild@v1 93 | 94 | - name: Build 95 | run: | 96 | msbuild "XoW.sln" /restore /m /t:Rebuild /nr:false /verbosity:minimal /p:RestoreLockedMode=true /p:AppxBundle=Always /p:UapAppxPackageBuildMode=StoreUpload /p:AppxBundlePlatforms="x86|x64|ARM" /p:configuration="Debug" /p:VisualStudioVersion="17.0" 97 | 98 | - name: Perform CodeQL Analysis 99 | uses: github/codeql-action/analyze@v2 100 | with: 101 | category: "/language:csharp" 102 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release packages 2 | 3 | on: 4 | push: 5 | tags: [ "v*.*.*" ] 6 | 7 | jobs: 8 | build: 9 | name: Build packages 10 | runs-on: windows-latest 11 | env: 12 | APP_PACKAGES_DIRECTORY: $pwd\AppPackages 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Bump version 17 | shell: pwsh 18 | run: | 19 | $tag = "${{ github.ref_name }}" 20 | $tag = $tag.substring(1) 21 | $version = "$($tag).0" 22 | [xml]$manifest = Get-Content "$pwd\XoW\Package.appxmanifest" 23 | $manifest.Package.Identity.Version = "$($version)" 24 | $manifest.save("$pwd\XoW\Package.appxmanifest") 25 | echo "VERSION=$($version)" >> $env:GITHUB_ENV 26 | 27 | - name: Push changes 28 | run: | 29 | git config --global user.name "github-actions[bot]" 30 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 31 | git commit -a -m "chore: bump version" 32 | git push origin HEAD:master 33 | 34 | - name: Install signing certificate 35 | shell: pwsh 36 | env: 37 | PFX_CONTENT: ${{ secrets.SIGNING_CERTIFICATE }} 38 | SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD }} 39 | run: | 40 | echo "Saving the certificate to $($env:RUNNER_TEMP)"; 41 | $pfxPath = Join-Path -Path $env:RUNNER_TEMP -ChildPath "cert.pfx"; 42 | $encodedBytes = [System.Convert]::FromBase64String($env:PFX_CONTENT); 43 | Set-Content $pfxPath -Value $encodedBytes -AsByteStream; 44 | echo "Installing certificate from $($env:RUNNER_TEMP)\cert.pfx"; 45 | Import-PfxCertificate -FilePath "$($env:RUNNER_TEMP)\cert.pfx" -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:SIGNING_CERTIFICATE_PASSWORD -Force -AsPlainText); 46 | echo "CERT_PATH=$($pfxPath -replace '\\','/')" >> $env:GITHUB_ENV 47 | 48 | - name: Add AppConfig.xml to working directory 49 | shell: pwsh 50 | env: 51 | APP_CONFIG_XML: ${{ secrets.APP_CONFIG_XML }} 52 | run: | 53 | echo "Adding AppConfig.xml to $($env:GITHUB_WORKSPACE)\XoW"; 54 | $filePath = Join-Path -Path "$($env:GITHUB_WORKSPACE)\XoW" -ChildPath "AppConfig.xml"; 55 | $encodedBytes = [System.Convert]::FromBase64String($env:APP_CONFIG_XML); 56 | Set-Content $filePath -Value $encodedBytes -AsByteStream; 57 | 58 | - name: Setup MSBuild 59 | uses: microsoft/setup-msbuild@v1 60 | 61 | - name: Build packages 62 | shell: pwsh 63 | run: >- 64 | msbuild 65 | "XoW.sln" 66 | /restore 67 | /m 68 | /verbosity:minimal 69 | /target:Rebuild 70 | /nodeReuse:false 71 | /p:AppxBundle=Always 72 | /p:AppxPackageDir="${{ env.APP_PACKAGES_DIRECTORY }}" 73 | /p:UapAppxPackageBuildMode=StoreAndSideload 74 | /p:AppxBundlePlatforms="x86|x64|ARM|ARM64" 75 | /p:configuration="Release" 76 | /p:VisualStudioVersion="17.0" 77 | 78 | - name: Upload release artifacts 79 | uses: softprops/action-gh-release@v1 80 | with: 81 | files: | 82 | ./AppPackages/XoW_${{ env.VERSION }}_Test/XoW_${{ env.VERSION }}_x86_x64_arm_arm64.cer 83 | ./AppPackages/XoW_${{ env.VERSION }}_Test/XoW_${{ env.VERSION }}_x86_x64_arm_arm64.msixbundle 84 | ./AppPackages/XoW_${{ env.VERSION }}_x86_x64_arm_arm64_bundle.msixupload 85 | -------------------------------------------------------------------------------- /.github/workflows/sync_to_gitee.yml: -------------------------------------------------------------------------------- 1 | name: Sync to GitEE 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | sync: 9 | name: Sync to GitEE 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Sync to GitEE 13 | uses: wearerequired/git-mirror-action@master 14 | env: 15 | SSH_PRIVATE_KEY: ${{ secrets.RSA_PRIVATE_KEY }} 16 | with: 17 | source-repo: git@github.com:boris1993/XoW.git 18 | destination-repo: git@gitee.com:boris1993/XoW.git 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/csharp,visualstudio,rider 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=csharp,visualstudio,rider 3 | 4 | ### Csharp ### 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | ## 8 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 9 | 10 | # Local config file 11 | AppConfig.xml 12 | 13 | # User-specific files 14 | *.rsuser 15 | *.suo 16 | *.user 17 | *.userosscache 18 | *.sln.docstates 19 | 20 | # User-specific files (MonoDevelop/Xamarin Studio) 21 | *.userprefs 22 | 23 | # Mono auto generated files 24 | mono_crash.* 25 | 26 | # Build results 27 | [Dd]ebug/ 28 | [Dd]ebugPublic/ 29 | [Rr]elease/ 30 | [Rr]eleases/ 31 | x64/ 32 | x86/ 33 | [Ww][Ii][Nn]32/ 34 | [Aa][Rr][Mm]/ 35 | [Aa][Rr][Mm]64/ 36 | bld/ 37 | [Bb]in/ 38 | [Oo]bj/ 39 | [Ll]og/ 40 | [Ll]ogs/ 41 | 42 | # Visual Studio 2015/2017 cache/options directory 43 | .vs/ 44 | # Uncomment if you have tasks that create the project's static files in wwwroot 45 | #wwwroot/ 46 | 47 | # Visual Studio 2017 auto generated files 48 | Generated\ Files/ 49 | 50 | # MSTest test Results 51 | [Tt]est[Rr]esult*/ 52 | [Bb]uild[Ll]og.* 53 | 54 | # NUnit 55 | *.VisualState.xml 56 | TestResult.xml 57 | nunit-*.xml 58 | 59 | # Build Results of an ATL Project 60 | [Dd]ebugPS/ 61 | [Rr]eleasePS/ 62 | dlldata.c 63 | 64 | # Benchmark Results 65 | BenchmarkDotNet.Artifacts/ 66 | 67 | # .NET Core 68 | project.lock.json 69 | project.fragment.lock.json 70 | artifacts/ 71 | 72 | # ASP.NET Scaffolding 73 | ScaffoldingReadMe.txt 74 | 75 | # StyleCop 76 | StyleCopReport.xml 77 | 78 | # Files built by Visual Studio 79 | *_i.c 80 | *_p.c 81 | *_h.h 82 | *.ilk 83 | *.meta 84 | *.obj 85 | *.iobj 86 | *.pch 87 | *.pdb 88 | *.ipdb 89 | *.pgc 90 | *.pgd 91 | *.rsp 92 | *.sbr 93 | *.tlb 94 | *.tli 95 | *.tlh 96 | *.tmp 97 | *.tmp_proj 98 | *_wpftmp.csproj 99 | *.log 100 | *.tlog 101 | *.vspscc 102 | *.vssscc 103 | .builds 104 | *.pidb 105 | *.svclog 106 | *.scc 107 | 108 | # Chutzpah Test files 109 | _Chutzpah* 110 | 111 | # Visual C++ cache files 112 | ipch/ 113 | *.aps 114 | *.ncb 115 | *.opendb 116 | *.opensdf 117 | *.sdf 118 | *.cachefile 119 | *.VC.db 120 | *.VC.VC.opendb 121 | 122 | # Visual Studio profiler 123 | *.psess 124 | *.vsp 125 | *.vspx 126 | *.sap 127 | 128 | # Visual Studio Trace Files 129 | *.e2e 130 | 131 | # TFS 2012 Local Workspace 132 | $tf/ 133 | 134 | # Guidance Automation Toolkit 135 | *.gpState 136 | 137 | # ReSharper is a .NET coding add-in 138 | _ReSharper*/ 139 | *.[Rr]e[Ss]harper 140 | *.DotSettings.user 141 | 142 | # TeamCity is a build add-in 143 | _TeamCity* 144 | 145 | # DotCover is a Code Coverage Tool 146 | *.dotCover 147 | 148 | # AxoCover is a Code Coverage Tool 149 | .axoCover/* 150 | !.axoCover/settings.json 151 | 152 | # Coverlet is a free, cross platform Code Coverage Tool 153 | coverage*.json 154 | coverage*.xml 155 | coverage*.info 156 | 157 | # Visual Studio code coverage results 158 | *.coverage 159 | *.coveragexml 160 | 161 | # NCrunch 162 | _NCrunch_* 163 | .*crunch*.local.xml 164 | nCrunchTemp_* 165 | 166 | # MightyMoose 167 | *.mm.* 168 | AutoTest.Net/ 169 | 170 | # Web workbench (sass) 171 | .sass-cache/ 172 | 173 | # Installshield output folder 174 | [Ee]xpress/ 175 | 176 | # DocProject is a documentation generator add-in 177 | DocProject/buildhelp/ 178 | DocProject/Help/*.HxT 179 | DocProject/Help/*.HxC 180 | DocProject/Help/*.hhc 181 | DocProject/Help/*.hhk 182 | DocProject/Help/*.hhp 183 | DocProject/Help/Html2 184 | DocProject/Help/html 185 | 186 | # Click-Once directory 187 | publish/ 188 | 189 | # Publish Web Output 190 | *.[Pp]ublish.xml 191 | *.azurePubxml 192 | # Note: Comment the next line if you want to checkin your web deploy settings, 193 | # but database connection strings (with potential passwords) will be unencrypted 194 | *.pubxml 195 | *.publishproj 196 | 197 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 198 | # checkin your Azure Web App publish settings, but sensitive information contained 199 | # in these scripts will be unencrypted 200 | PublishScripts/ 201 | 202 | # NuGet Packages 203 | *.nupkg 204 | # NuGet Symbol Packages 205 | *.snupkg 206 | # The packages folder can be ignored because of Package Restore 207 | **/[Pp]ackages/* 208 | # except build/, which is used as an MSBuild target. 209 | !**/[Pp]ackages/build/ 210 | # Uncomment if necessary however generally it will be regenerated when needed 211 | #!**/[Pp]ackages/repositories.config 212 | # NuGet v3's project.json files produces more ignorable files 213 | *.nuget.props 214 | *.nuget.targets 215 | 216 | # Microsoft Azure Build Output 217 | csx/ 218 | *.build.csdef 219 | 220 | # Microsoft Azure Emulator 221 | ecf/ 222 | rcf/ 223 | 224 | # Windows Store app package directories and files 225 | AppPackages/ 226 | BundleArtifacts/ 227 | Package.StoreAssociation.xml 228 | _pkginfo.txt 229 | *.appx 230 | *.appxbundle 231 | *.appxupload 232 | 233 | # Visual Studio cache files 234 | # files ending in .cache can be ignored 235 | *.[Cc]ache 236 | # but keep track of directories ending in .cache 237 | !?*.[Cc]ache/ 238 | 239 | # Others 240 | ClientBin/ 241 | ~$* 242 | *~ 243 | *.dbmdl 244 | *.dbproj.schemaview 245 | *.jfm 246 | *.pfx 247 | *.publishsettings 248 | orleans.codegen.cs 249 | 250 | # Including strong name files can present a security risk 251 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 252 | #*.snk 253 | 254 | # Since there are multiple workflows, uncomment next line to ignore bower_components 255 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 256 | #bower_components/ 257 | 258 | # RIA/Silverlight projects 259 | Generated_Code/ 260 | 261 | # Backup & report files from converting an old project file 262 | # to a newer Visual Studio version. Backup files are not needed, 263 | # because we have git ;-) 264 | _UpgradeReport_Files/ 265 | Backup*/ 266 | UpgradeLog*.XML 267 | UpgradeLog*.htm 268 | ServiceFabricBackup/ 269 | *.rptproj.bak 270 | 271 | # SQL Server files 272 | *.mdf 273 | *.ldf 274 | *.ndf 275 | 276 | # Business Intelligence projects 277 | *.rdl.data 278 | *.bim.layout 279 | *.bim_*.settings 280 | *.rptproj.rsuser 281 | *- [Bb]ackup.rdl 282 | *- [Bb]ackup ([0-9]).rdl 283 | *- [Bb]ackup ([0-9][0-9]).rdl 284 | 285 | # Microsoft Fakes 286 | FakesAssemblies/ 287 | 288 | # GhostDoc plugin setting file 289 | *.GhostDoc.xml 290 | 291 | # Node.js Tools for Visual Studio 292 | .ntvs_analysis.dat 293 | node_modules/ 294 | 295 | # Visual Studio 6 build log 296 | *.plg 297 | 298 | # Visual Studio 6 workspace options file 299 | *.opt 300 | 301 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 302 | *.vbw 303 | 304 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 305 | *.vbp 306 | 307 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 308 | *.dsw 309 | *.dsp 310 | 311 | # Visual Studio 6 technical files 312 | 313 | # Visual Studio LightSwitch build output 314 | **/*.HTMLClient/GeneratedArtifacts 315 | **/*.DesktopClient/GeneratedArtifacts 316 | **/*.DesktopClient/ModelManifest.xml 317 | **/*.Server/GeneratedArtifacts 318 | **/*.Server/ModelManifest.xml 319 | _Pvt_Extensions 320 | 321 | # Paket dependency manager 322 | .paket/paket.exe 323 | paket-files/ 324 | 325 | # FAKE - F# Make 326 | .fake/ 327 | 328 | # CodeRush personal settings 329 | .cr/personal 330 | 331 | # Python Tools for Visual Studio (PTVS) 332 | __pycache__/ 333 | *.pyc 334 | 335 | # Cake - Uncomment if you are using it 336 | # tools/** 337 | # !tools/packages.config 338 | 339 | # Tabs Studio 340 | *.tss 341 | 342 | # Telerik's JustMock configuration file 343 | *.jmconfig 344 | 345 | # BizTalk build output 346 | *.btp.cs 347 | *.btm.cs 348 | *.odx.cs 349 | *.xsd.cs 350 | 351 | # OpenCover UI analysis results 352 | OpenCover/ 353 | 354 | # Azure Stream Analytics local run output 355 | ASALocalRun/ 356 | 357 | # MSBuild Binary and Structured Log 358 | *.binlog 359 | 360 | # NVidia Nsight GPU debugger configuration file 361 | *.nvuser 362 | 363 | # MFractors (Xamarin productivity tool) working folder 364 | .mfractor/ 365 | 366 | # Local History for Visual Studio 367 | .localhistory/ 368 | 369 | # Visual Studio History (VSHistory) files 370 | .vshistory/ 371 | 372 | # BeatPulse healthcheck temp database 373 | healthchecksdb 374 | 375 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 376 | MigrationBackup/ 377 | 378 | # Ionide (cross platform F# VS Code tools) working folder 379 | .ionide/ 380 | 381 | # Fody - auto-generated XML schema 382 | FodyWeavers.xsd 383 | 384 | # VS Code files for those working on multiple tools 385 | .vscode/* 386 | !.vscode/settings.json 387 | !.vscode/tasks.json 388 | !.vscode/launch.json 389 | !.vscode/extensions.json 390 | *.code-workspace 391 | 392 | # Local History for Visual Studio Code 393 | .history/ 394 | 395 | # Windows Installer files from build outputs 396 | *.cab 397 | *.msi 398 | *.msix 399 | *.msm 400 | *.msp 401 | 402 | # JetBrains Rider 403 | *.sln.iml 404 | 405 | ### Rider ### 406 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 407 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 408 | 409 | # User-specific stuff 410 | .idea/**/workspace.xml 411 | .idea/**/tasks.xml 412 | .idea/**/usage.statistics.xml 413 | .idea/**/dictionaries 414 | .idea/**/shelf 415 | 416 | # AWS User-specific 417 | .idea/**/aws.xml 418 | 419 | # Generated files 420 | .idea/**/contentModel.xml 421 | 422 | # Sensitive or high-churn files 423 | .idea/**/dataSources/ 424 | .idea/**/dataSources.ids 425 | .idea/**/dataSources.local.xml 426 | .idea/**/sqlDataSources.xml 427 | .idea/**/dynamic.xml 428 | .idea/**/uiDesigner.xml 429 | .idea/**/dbnavigator.xml 430 | 431 | # Gradle 432 | .idea/**/gradle.xml 433 | .idea/**/libraries 434 | 435 | # Gradle and Maven with auto-import 436 | # When using Gradle or Maven with auto-import, you should exclude module files, 437 | # since they will be recreated, and may cause churn. Uncomment if using 438 | # auto-import. 439 | # .idea/artifacts 440 | # .idea/compiler.xml 441 | # .idea/jarRepositories.xml 442 | # .idea/modules.xml 443 | # .idea/*.iml 444 | # .idea/modules 445 | # *.iml 446 | # *.ipr 447 | 448 | # CMake 449 | cmake-build-*/ 450 | 451 | # Mongo Explorer plugin 452 | .idea/**/mongoSettings.xml 453 | 454 | # File-based project format 455 | *.iws 456 | 457 | # IntelliJ 458 | out/ 459 | 460 | # mpeltonen/sbt-idea plugin 461 | .idea_modules/ 462 | 463 | # JIRA plugin 464 | atlassian-ide-plugin.xml 465 | 466 | # Cursive Clojure plugin 467 | .idea/replstate.xml 468 | 469 | # SonarLint plugin 470 | .idea/sonarlint/ 471 | 472 | # Crashlytics plugin (for Android Studio and IntelliJ) 473 | com_crashlytics_export_strings.xml 474 | crashlytics.properties 475 | crashlytics-build.properties 476 | fabric.properties 477 | 478 | # Editor-based Rest Client 479 | .idea/httpRequests 480 | 481 | # Android studio 3.1+ serialized cache file 482 | .idea/caches/build_file_checksums.ser 483 | 484 | ### VisualStudio ### 485 | 486 | # User-specific files 487 | 488 | # User-specific files (MonoDevelop/Xamarin Studio) 489 | 490 | # Mono auto generated files 491 | 492 | # Build results 493 | 494 | # Visual Studio 2015/2017 cache/options directory 495 | # Uncomment if you have tasks that create the project's static files in wwwroot 496 | 497 | # Visual Studio 2017 auto generated files 498 | 499 | # MSTest test Results 500 | 501 | # NUnit 502 | 503 | # Build Results of an ATL Project 504 | 505 | # Benchmark Results 506 | 507 | # .NET Core 508 | 509 | # ASP.NET Scaffolding 510 | 511 | # StyleCop 512 | 513 | # Files built by Visual Studio 514 | 515 | # Chutzpah Test files 516 | 517 | # Visual C++ cache files 518 | 519 | # Visual Studio profiler 520 | 521 | # Visual Studio Trace Files 522 | 523 | # TFS 2012 Local Workspace 524 | 525 | # Guidance Automation Toolkit 526 | 527 | # ReSharper is a .NET coding add-in 528 | 529 | # TeamCity is a build add-in 530 | 531 | # DotCover is a Code Coverage Tool 532 | 533 | # AxoCover is a Code Coverage Tool 534 | 535 | # Coverlet is a free, cross platform Code Coverage Tool 536 | 537 | # Visual Studio code coverage results 538 | 539 | # NCrunch 540 | 541 | # MightyMoose 542 | 543 | # Web workbench (sass) 544 | 545 | # Installshield output folder 546 | 547 | # DocProject is a documentation generator add-in 548 | 549 | # Click-Once directory 550 | 551 | # Publish Web Output 552 | # Note: Comment the next line if you want to checkin your web deploy settings, 553 | # but database connection strings (with potential passwords) will be unencrypted 554 | 555 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 556 | # checkin your Azure Web App publish settings, but sensitive information contained 557 | # in these scripts will be unencrypted 558 | 559 | # NuGet Packages 560 | # NuGet Symbol Packages 561 | # The packages folder can be ignored because of Package Restore 562 | # except build/, which is used as an MSBuild target. 563 | # Uncomment if necessary however generally it will be regenerated when needed 564 | # NuGet v3's project.json files produces more ignorable files 565 | 566 | # Microsoft Azure Build Output 567 | 568 | # Microsoft Azure Emulator 569 | 570 | # Windows Store app package directories and files 571 | 572 | # Visual Studio cache files 573 | # files ending in .cache can be ignored 574 | # but keep track of directories ending in .cache 575 | 576 | # Others 577 | 578 | # Including strong name files can present a security risk 579 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 580 | 581 | # Since there are multiple workflows, uncomment next line to ignore bower_components 582 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 583 | 584 | # RIA/Silverlight projects 585 | 586 | # Backup & report files from converting an old project file 587 | # to a newer Visual Studio version. Backup files are not needed, 588 | # because we have git ;-) 589 | 590 | # SQL Server files 591 | 592 | # Business Intelligence projects 593 | 594 | # Microsoft Fakes 595 | 596 | # GhostDoc plugin setting file 597 | 598 | # Node.js Tools for Visual Studio 599 | 600 | # Visual Studio 6 build log 601 | 602 | # Visual Studio 6 workspace options file 603 | 604 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 605 | 606 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 607 | 608 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 609 | 610 | # Visual Studio 6 technical files 611 | 612 | # Visual Studio LightSwitch build output 613 | 614 | # Paket dependency manager 615 | 616 | # FAKE - F# Make 617 | 618 | # CodeRush personal settings 619 | 620 | # Python Tools for Visual Studio (PTVS) 621 | 622 | # Cake - Uncomment if you are using it 623 | # tools/** 624 | # !tools/packages.config 625 | 626 | # Tabs Studio 627 | 628 | # Telerik's JustMock configuration file 629 | 630 | # BizTalk build output 631 | 632 | # OpenCover UI analysis results 633 | 634 | # Azure Stream Analytics local run output 635 | 636 | # MSBuild Binary and Structured Log 637 | 638 | # NVidia Nsight GPU debugger configuration file 639 | 640 | # MFractors (Xamarin productivity tool) working folder 641 | 642 | # Local History for Visual Studio 643 | 644 | # Visual Studio History (VSHistory) files 645 | 646 | # BeatPulse healthcheck temp database 647 | 648 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 649 | 650 | # Ionide (cross platform F# VS Code tools) working folder 651 | 652 | # Fody - auto-generated XML schema 653 | 654 | # VS Code files for those working on multiple tools 655 | 656 | # Local History for Visual Studio Code 657 | 658 | # Windows Installer files from build outputs 659 | 660 | # JetBrains Rider 661 | 662 | ### VisualStudio Patch ### 663 | # Additional files built by Visual Studio 664 | 665 | # End of https://www.toptal.com/developers/gitignore/api/csharp,visualstudio,rider -------------------------------------------------------------------------------- /Get_it_from_Microsoft_Badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/Get_it_from_Microsoft_Badge.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # X岛 on Windows 2 | 3 | [![Build packages](https://github.com/boris1993/XoW/actions/workflows/build.yml/badge.svg)](https://github.com/boris1993/XoW/actions/workflows/build.yml) 4 | [![Code analysis](https://github.com/boris1993/XoW/actions/workflows/code_analysis.yml/badge.svg)](https://github.com/boris1993/XoW/actions/workflows/code_analysis.yml) 5 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fboris1993%2FXoW.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fboris1993%2FXoW?ref=badge_shield) 6 | [![GitHub release](https://img.shields.io/github/v/release/boris1993/XoW)](https://github.com/boris1993/XoW/releases/latest) 7 | 8 | [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=boris1993_XoW&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=boris1993_XoW) 9 | [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=boris1993_XoW&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=boris1993_XoW) 10 | [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=boris1993_XoW&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=boris1993_XoW) 11 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=boris1993_XoW&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=boris1993_XoW) 12 | [![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=boris1993_XoW&metric=sqale_index)](https://sonarcloud.io/summary/new_code?id=boris1993_XoW) 13 | 14 | 一个开放源代码的X岛客户端,基于UWP开发。 15 | 16 | [![](Get_it_from_Microsoft_Badge.png)](https://www.microsoft.com/store/apps/9N8NZJ88HF6C) 17 | 18 | # 开放源代码许可 19 | 20 | 该项目根据[MPL 2.0协议](LICENSE.txt)开放源代码。同时,该代码不希望被用于一切有关于“阿苇岛匿名版”的开发。 21 | 22 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fboris1993%2FXoW.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fboris1993%2FXoW?ref=badge_large) 23 | -------------------------------------------------------------------------------- /XoW.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32616.157 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XoW", "XoW\XoW.csproj", "{4D0FA67F-B8CC-45DB-9D72-C47735FC9380}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|ARM = Debug|ARM 11 | Debug|ARM64 = Debug|ARM64 12 | Debug|x64 = Debug|x64 13 | Debug|x86 = Debug|x86 14 | Release|ARM = Release|ARM 15 | Release|ARM64 = Release|ARM64 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Debug|ARM.ActiveCfg = Debug|ARM 21 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Debug|ARM.Build.0 = Debug|ARM 22 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Debug|ARM.Deploy.0 = Debug|ARM 23 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Debug|ARM64.ActiveCfg = Debug|ARM64 24 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Debug|ARM64.Build.0 = Debug|ARM64 25 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Debug|ARM64.Deploy.0 = Debug|ARM64 26 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Debug|x64.ActiveCfg = Debug|x64 27 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Debug|x64.Build.0 = Debug|x64 28 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Debug|x64.Deploy.0 = Debug|x64 29 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Debug|x86.ActiveCfg = Debug|x86 30 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Debug|x86.Build.0 = Debug|x86 31 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Debug|x86.Deploy.0 = Debug|x86 32 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Release|ARM.ActiveCfg = Release|ARM 33 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Release|ARM.Build.0 = Release|ARM 34 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Release|ARM.Deploy.0 = Release|ARM 35 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Release|ARM64.ActiveCfg = Release|ARM64 36 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Release|ARM64.Build.0 = Release|ARM64 37 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Release|ARM64.Deploy.0 = Release|ARM64 38 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Release|x64.ActiveCfg = Release|x64 39 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Release|x64.Build.0 = Release|x64 40 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Release|x64.Deploy.0 = Release|x64 41 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Release|x86.ActiveCfg = Release|x86 42 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Release|x86.Build.0 = Release|x86 43 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380}.Release|x86.Deploy.0 = Release|x86 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(ExtensibilityGlobals) = postSolution 49 | SolutionGuid = {F6AE123C-65B7-4523-AEAF-C00015E1A314} 50 | EndGlobalSection 51 | EndGlobal 52 | -------------------------------------------------------------------------------- /XoW.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True -------------------------------------------------------------------------------- /XoW/App.xaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /XoW/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Windows.ApplicationModel; 3 | using Windows.ApplicationModel.Activation; 4 | using Windows.UI.Xaml; 5 | using Windows.UI.Xaml.Controls; 6 | using Windows.UI.Xaml.Navigation; 7 | using XoW.Services; 8 | using XoW.Views; 9 | using UnhandledExceptionEventArgs = Windows.UI.Xaml.UnhandledExceptionEventArgs; 10 | 11 | namespace XoW 12 | { 13 | /// 14 | /// Provides application-specific behavior to supplement the default Application class. 15 | /// 16 | sealed partial class App : Application 17 | { 18 | /// 19 | /// Initializes the singleton application object. This is the first line of authored code 20 | /// executed, and as such is the logical equivalent of main() or WinMain(). 21 | /// 22 | public App() 23 | { 24 | InitializeComponent(); 25 | Suspending += OnSuspending; 26 | UnhandledException += OnUnhandledException; 27 | 28 | ConfigurationManager.LoadAppConfig(); 29 | } 30 | 31 | /// 32 | /// Invoked when the application is launched normally by the end user. Other entry points 33 | /// will be used such as when the application is launched to open a specific file. 34 | /// 35 | /// Details about the launch request and process. 36 | protected override void OnLaunched(LaunchActivatedEventArgs e) 37 | { 38 | var rootFrame = Window.Current.Content as Frame; 39 | 40 | // Do not repeat app initialization when the Window already has content, 41 | // just ensure that the window is active 42 | if (rootFrame == null) 43 | { 44 | // Create a Frame to act as the navigation context and navigate to the first page 45 | rootFrame = new Frame(); 46 | 47 | rootFrame.NavigationFailed += OnNavigationFailed; 48 | 49 | if (e.PreviousExecutionState == ApplicationExecutionState.Terminated) 50 | { 51 | //TODO: Load state from previously suspended application 52 | } 53 | 54 | // Place the frame in the current Window 55 | Window.Current.Content = rootFrame; 56 | } 57 | 58 | if (e.PrelaunchActivated == false) 59 | { 60 | if (rootFrame.Content == null) 61 | { 62 | // When the navigation stack isn't restored navigate to the first page, 63 | // configuring the new page by passing required information as a navigation 64 | // parameter 65 | rootFrame.Navigate(typeof(MainPage), e.Arguments); 66 | } 67 | // Ensure the current window is active 68 | Window.Current.Activate(); 69 | } 70 | } 71 | 72 | /// 73 | /// Invoked when Navigation to a certain page fails 74 | /// 75 | /// The Frame which failed navigation 76 | /// Details about the navigation failure 77 | private void OnNavigationFailed(object sender, NavigationFailedEventArgs e) => throw new Exception("Failed to load Page " + e.SourcePageType.FullName); 78 | 79 | /// 80 | /// Invoked when application execution is being suspended. Application state is saved 81 | /// without knowing whether the application will be terminated or resumed with the contents 82 | /// of memory still intact. 83 | /// 84 | /// The source of the suspend request. 85 | /// Details about the suspend request. 86 | private void OnSuspending(object sender, SuspendingEventArgs e) 87 | { 88 | var deferral = e.SuspendingOperation.GetDeferral(); 89 | //TODO: Save application state and stop any background activity 90 | deferral.Complete(); 91 | } 92 | 93 | private async void OnUnhandledException(object sender, UnhandledExceptionEventArgs args) 94 | { 95 | args.Handled = true; 96 | var errorMessage = args.Exception.InnerException?.Message ?? args.Message; 97 | 98 | await new NotificationContentDialog(true, errorMessage).ShowAsync(); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /XoW/AppConfig.template.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /XoW/Assets/BadgeLogo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/BadgeLogo.scale-100.png -------------------------------------------------------------------------------- /XoW/Assets/BadgeLogo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/BadgeLogo.scale-125.png -------------------------------------------------------------------------------- /XoW/Assets/BadgeLogo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/BadgeLogo.scale-150.png -------------------------------------------------------------------------------- /XoW/Assets/BadgeLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/BadgeLogo.scale-200.png -------------------------------------------------------------------------------- /XoW/Assets/BadgeLogo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/BadgeLogo.scale-400.png -------------------------------------------------------------------------------- /XoW/Assets/LargeTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/LargeTile.scale-100.png -------------------------------------------------------------------------------- /XoW/Assets/LargeTile.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/LargeTile.scale-125.png -------------------------------------------------------------------------------- /XoW/Assets/LargeTile.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/LargeTile.scale-150.png -------------------------------------------------------------------------------- /XoW/Assets/LargeTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/LargeTile.scale-200.png -------------------------------------------------------------------------------- /XoW/Assets/LargeTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/LargeTile.scale-400.png -------------------------------------------------------------------------------- /XoW/Assets/SmallTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/SmallTile.scale-100.png -------------------------------------------------------------------------------- /XoW/Assets/SmallTile.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/SmallTile.scale-125.png -------------------------------------------------------------------------------- /XoW/Assets/SmallTile.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/SmallTile.scale-150.png -------------------------------------------------------------------------------- /XoW/Assets/SmallTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/SmallTile.scale-200.png -------------------------------------------------------------------------------- /XoW/Assets/SmallTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/SmallTile.scale-400.png -------------------------------------------------------------------------------- /XoW/Assets/SplashScreen.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/SplashScreen.scale-100.png -------------------------------------------------------------------------------- /XoW/Assets/SplashScreen.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/SplashScreen.scale-125.png -------------------------------------------------------------------------------- /XoW/Assets/SplashScreen.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/SplashScreen.scale-150.png -------------------------------------------------------------------------------- /XoW/Assets/SplashScreen.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/SplashScreen.scale-200.png -------------------------------------------------------------------------------- /XoW/Assets/SplashScreen.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/SplashScreen.scale-400.png -------------------------------------------------------------------------------- /XoW/Assets/Square150x150Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square150x150Logo.scale-100.png -------------------------------------------------------------------------------- /XoW/Assets/Square150x150Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square150x150Logo.scale-125.png -------------------------------------------------------------------------------- /XoW/Assets/Square150x150Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square150x150Logo.scale-150.png -------------------------------------------------------------------------------- /XoW/Assets/Square150x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square150x150Logo.scale-200.png -------------------------------------------------------------------------------- /XoW/Assets/Square150x150Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square150x150Logo.scale-400.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.altform-unplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.altform-unplated_targetsize-16.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.altform-unplated_targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.altform-unplated_targetsize-24.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.altform-unplated_targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.altform-unplated_targetsize-256.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.altform-unplated_targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.altform-unplated_targetsize-32.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.altform-unplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.altform-unplated_targetsize-48.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.scale-100.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.scale-125.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.scale-150.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.scale-200.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.scale-400.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.targetsize-16.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.targetsize-24.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.targetsize-256.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.targetsize-32.png -------------------------------------------------------------------------------- /XoW/Assets/Square44x44Logo.targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Square44x44Logo.targetsize-48.png -------------------------------------------------------------------------------- /XoW/Assets/StoreLogo.backup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/StoreLogo.backup.png -------------------------------------------------------------------------------- /XoW/Assets/StoreLogo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/StoreLogo.scale-100.png -------------------------------------------------------------------------------- /XoW/Assets/StoreLogo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/StoreLogo.scale-125.png -------------------------------------------------------------------------------- /XoW/Assets/StoreLogo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/StoreLogo.scale-150.png -------------------------------------------------------------------------------- /XoW/Assets/StoreLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/StoreLogo.scale-200.png -------------------------------------------------------------------------------- /XoW/Assets/StoreLogo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/StoreLogo.scale-400.png -------------------------------------------------------------------------------- /XoW/Assets/Wide310x150Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Wide310x150Logo.scale-100.png -------------------------------------------------------------------------------- /XoW/Assets/Wide310x150Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Wide310x150Logo.scale-125.png -------------------------------------------------------------------------------- /XoW/Assets/Wide310x150Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Wide310x150Logo.scale-150.png -------------------------------------------------------------------------------- /XoW/Assets/Wide310x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Wide310x150Logo.scale-200.png -------------------------------------------------------------------------------- /XoW/Assets/Wide310x150Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris1993/XoW/fb02a0cb330d8915038a743577773ad1bfe30943/XoW/Assets/Wide310x150Logo.scale-400.png -------------------------------------------------------------------------------- /XoW/Constants.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Immutable; 3 | 4 | namespace XoW 5 | { 6 | public static class Url 7 | { 8 | /// 9 | /// X岛域名 10 | /// 11 | private const string DomainName = "www.nmbxd1.com"; 12 | 13 | private const string BaseUrl = $"https://{DomainName}"; 14 | 15 | /// 16 | /// 获取CDN列表 17 | /// 18 | public const string GetCdn = $"{BaseUrl}/Api/getCdnPath"; 19 | 20 | /// 21 | /// 获取板块列表 22 | /// 23 | public const string GetForums = $"{BaseUrl}/Api/getForumList"; 24 | 25 | /// 26 | /// 获取时间线
27 | /// 参数page为分页页码 28 | ///
29 | public const string GetTimeline = $"{BaseUrl}/Api/Timeline"; 30 | 31 | /// 32 | /// 获取板块中的串
33 | /// 参数id为板块ID
34 | /// 参数page为分页页码 35 | ///
36 | public const string GetThreads = $"{BaseUrl}/Api/showf"; 37 | 38 | /// 39 | /// 获取串和回复
40 | /// 参数id为串号
41 | /// 参数page为分页页码 42 | ///
43 | public const string GetReplies = $"{BaseUrl}/Api/thread"; 44 | 45 | /// 46 | /// 只看po
47 | /// 参数id为串号
48 | /// 参数page为分页页码 49 | ///
50 | public const string GetPoOnlyReplies = $"{BaseUrl}/Api/po"; 51 | 52 | /// 53 | /// 获取订阅
54 | /// 参数uuid为订阅ID
55 | /// 参数page为分页页码 56 | ///
57 | public const string GetSubscription = $"{BaseUrl}/Api/feed"; 58 | 59 | /// 60 | /// 添加订阅
61 | /// 参数uuid为订阅ID
62 | /// 参数tid为串ID 63 | ///
64 | public const string AddSubscription = $"{BaseUrl}/Api/addFeed"; 65 | 66 | /// 67 | /// 删除订阅
68 | /// 参数uuid为订阅ID
69 | /// 参数tid为串ID 70 | ///
71 | public const string DeleteSubscription = $"{BaseUrl}/Api/delFeed"; 72 | 73 | /// 74 | /// 发新串
75 | ///
76 | public const string CreateNewThread = $"{BaseUrl}/Home/Forum/doPostThread.html"; 77 | 78 | /// 79 | /// 发回复 80 | /// 81 | public const string CreateNewReply = $"{BaseUrl}/Home/Forum/doReplyThread.html"; 82 | 83 | /// 84 | /// 搜索
85 | /// 参数q为关键词
86 | /// 参数page为页码
87 | ///
88 | public const string Search = $"{BaseUrl}/Api/search"; 89 | 90 | public const string GetThreadReference = $"{BaseUrl}/Home/Forum/ref"; 91 | 92 | public const string GitHubRepo = "https://github.com/boris1993/XoW"; 93 | 94 | public const string AiFaDianHomepage = "https://afdian.net/@xdao4windows"; 95 | } 96 | 97 | public static class Constants 98 | { 99 | public const string ForumName = "X岛匿名版"; 100 | 101 | public const string NoCookieSelected = "无生效的饼干"; 102 | 103 | public const string TimelineForumId = "-1"; 104 | 105 | public const string PermissionLevelCookieRequired = "2"; 106 | 107 | public const string CookieNameUserHash = "userhash"; 108 | 109 | public const string FavouriteThreadNavigationItemName = "收藏"; 110 | 111 | public const string Po = "(PO)"; 112 | 113 | /// 114 | /// 用于启动系统截图功能的URI
115 | /// 文档: 116 | ///
117 | public const string SystemUriStartScreenClip = "ms-screenclip:edit?clippingMode=Rectangle"; 118 | 119 | /// 120 | /// 颜文字列表 121 | /// key为名字 122 | /// value为实际的颜文字 123 | /// 124 | public static readonly ImmutableDictionary Emoticons = new Dictionary 125 | { 126 | {"|∀゚", "|∀゚"}, 127 | {"(´゚Д゚`)", "(´゚Д゚`)"}, 128 | {"(;´Д`)", "(;´Д`)"}, 129 | {"(`・ω・)", "(`・ω・)"}, 130 | {"(=゚ω゚)=", "(=゚ω゚)="}, 131 | {"| ω・´)", "| ω・´)"}, 132 | {"|-` )", "|-` )"}, 133 | {"|д` )", "|д` )"}, 134 | {"|ー` )", "|ー` )"}, 135 | {"|∀` )", "|∀` )"}, 136 | {"(つд⊂)", "(つд⊂)"}, 137 | {"(゚Д゚≡゚Д゚)", "(゚Д゚≡゚Д゚)"}, 138 | {"(^o^)ノ", "(^o^)ノ"}, 139 | {"(|||゚Д゚)", "(|||゚Д゚)"}, 140 | {"( ゚∀゚)", "( ゚∀゚)"}, 141 | {"( ´∀`)", "( ´∀`)"}, 142 | {"(*´∀`)", "(*´∀`)"}, 143 | {"(*゚∇゚)", "(*゚∇゚)"}, 144 | {"(*゚ー゚)", "(*゚ー゚)"}, 145 | {"( ゚ 3゚)", "( ゚ 3゚)"}, 146 | {"( ´ー`)", "( ´ー`)"}, 147 | {"( ・_ゝ・)", "( ・_ゝ・)"}, 148 | {"( ´_ゝ`)", "( ´_ゝ`)"}, 149 | {"(*´д`)", "(*´д`)"}, 150 | {"(・ー・)", "(・ー・)"}, 151 | {"(・∀・)", "(・∀・)"}, 152 | {"(ゝ∀・)", "(ゝ∀・)"}, 153 | {"(〃∀〃)", "(〃∀〃)"}, 154 | {"(*゚∀゚*)", "(*゚∀゚*)"}, 155 | {"( ゚∀。)", "( ゚∀。)"}, 156 | {"( `д´)", "( `д´)"}, 157 | {"(`ε´ )", "(`ε´ )"}, 158 | {"(`ヮ´ )", "(`ヮ´ )"}, 159 | {"σ`∀´)", "σ`∀´)"}, 160 | {" ゚∀゚)σ", " ゚∀゚)σ"}, 161 | {"゚ ∀゚)ノ", "゚ ∀゚)ノ"}, 162 | {"(╬゚д゚)", "(╬゚д゚)"}, 163 | {"(|||゚д゚)", "(|||゚д゚)"}, 164 | {"( ゚д゚)", "( ゚д゚)"}, 165 | {"Σ( ゚д゚)", "Σ( ゚д゚)"}, 166 | {"( ;゚д゚)", "( ;゚д゚)"}, 167 | {"( ;´д`)", "( ;´д`)"}, 168 | {"( д ) ゚ ゚", "( д ) ゚ ゚"}, 169 | {"( ☉д⊙)", "( ☉д⊙)"}, 170 | {"((( ゚д゚)))", "((( ゚д゚)))"}, 171 | {"( ` ・´)", "( ` ・´)"}, 172 | {"( ´д`)", "( ´д`)"}, 173 | {"( -д-)", "( -д-)"}, 174 | {"(>д<)", "(>д<)"}, 175 | {"・゚( ノд`゚)", "・゚( ノд`゚)"}, 176 | {"( TдT)", "( TдT)"}, 177 | {"( ̄∇ ̄)", "( ̄∇ ̄)"}, 178 | {"( ̄3 ̄)", "( ̄3 ̄)"}, 179 | {"( ̄ー ̄)", "( ̄ー ̄)"}, 180 | {"( ̄ .  ̄)", "( ̄ .  ̄)"}, 181 | {"( ̄皿 ̄)", "( ̄皿 ̄)"}, 182 | {"( ̄艸 ̄)", "( ̄艸 ̄)"}, 183 | {"( ̄︿ ̄)", "( ̄︿ ̄)"}, 184 | {"( ̄︶ ̄)", "( ̄︶ ̄)"}, 185 | {"ヾ(´ω゚`)", "ヾ(´ω゚`)"}, 186 | {"(*´ω`*)", "(*´ω`*)"}, 187 | {"(・ω・)", "(・ω・)"}, 188 | {"( ´・ω)", "( ´・ω)"}, 189 | {"(`・ω)", "(`・ω)"}, 190 | {"(´・ω・`)", "(´・ω・`)"}, 191 | {"(`・ω・´)", "(`・ω・´)"}, 192 | {"( `_っ´)", "( `_っ´)"}, 193 | {"( `ー´)", "( `ー´)"}, 194 | {"( ´_っ`)", "( ´_っ`)"}, 195 | {"( ´ρ`)", "( ´ρ`)"}, 196 | {"( ゚ω゚)", "( ゚ω゚)"}, 197 | {"(o゚ω゚o)", "(o゚ω゚o)"}, 198 | {"( ^ω^)", "( ^ω^)"}, 199 | {"(。◕∀◕。)", "(。◕∀◕。)"}, 200 | {"/( ◕‿‿◕ )\\", "/( ◕‿‿◕ )\\"}, 201 | {"ヾ(´ε`ヾ)", "ヾ(´ε`ヾ)"}, 202 | {"(ノ゚∀゚)ノ", "(ノ゚∀゚)ノ"}, 203 | {"(σ゚д゚)σ", "(σ゚д゚)σ"}, 204 | {"(σ゚∀゚)σ", "(σ゚∀゚)σ"}, 205 | {"|д゚ )", "|д゚ )"}, 206 | {"┃電柱┃", "┃電柱┃"}, 207 | {"゚(つд`゚)", "゚(つд`゚)"}, 208 | {"゚Å゚ ) ", "゚Å゚ ) "}, 209 | {"⊂彡☆))д`)", "⊂彡☆))д`)"}, 210 | {"⊂彡☆))д´)", "⊂彡☆))д´)"}, 211 | {"⊂彡☆))∀`)", "⊂彡☆))∀`)"}, 212 | {"(´∀((☆ミつ", "(´∀((☆ミつ"}, 213 | {"・゚( ノヮ´ )", "・゚( ノヮ´ )"}, 214 | {"(ノ)`ω´(ヾ)", "(ノ)`ω´(ヾ)"}, 215 | {"ᕕ( ᐛ )ᕗ", "ᕕ( ᐛ )ᕗ"}, 216 | {"( ˇωˇ)", "( ˇωˇ)"}, 217 | {"( 」゚Д゚)」<", "( 」゚Д゚)」<"}, 218 | {"( ›´ω`‹ )", "( ›´ω`‹ )"}, 219 | {"(;´ヮ`)7", "(;´ヮ`)7"}, 220 | {"(`ゥ´ )", "(`ゥ´ )"}, 221 | {"(`ᝫ´ )", "(`ᝫ´ )"}, 222 | {"( ᑭ`д´)ᓀ))д´)ᑫ", "( ᑭ`д´)ᓀ))д´)ᑫ"}, 223 | {"σ( ᑒ )", "σ( ᑒ )"}, 224 | {"齐齐蛤尔", "(`ヮ´ )σ`∀´) ゚∀゚)σ"}, 225 | { 226 | "大嘘", @"吁~~~~  rnm,退钱! 227 |    /   / 228 | ( ゚ 3゚) `ー´) `д´) `д´)" 229 | }, 230 | {"防剧透", "[h] [/h]"}, 231 | {"骰子", "[n]"}, 232 | {"高级骰子", "[n,m]"} 233 | }.ToImmutableDictionary(); 234 | } 235 | 236 | public static class ApplicationSettingsKey 237 | { 238 | public const string CurrentCookie = "current_cookie"; 239 | public const string AllCookies = "all_cookies"; 240 | public const string DarkThemeSelected = "dark_theme_selected"; 241 | public const string SubscriptionId = "subscription_id"; 242 | } 243 | 244 | public static class ComponentContent 245 | { 246 | public const string Ok = "知道了"; 247 | public const string Confirm = "我确定!"; 248 | public const string Cancel = "算了吧"; 249 | public const string SendReport = "提交举报"; 250 | public const string KeepingIt = "留着吧"; 251 | public const string DiscardIt = "不要了"; 252 | public const string Go = "走起!"; 253 | 254 | public const string Notification = "提示"; 255 | public const string Error = "错误"; 256 | 257 | public const string SubscriptionId = "订阅ID"; 258 | public const string GenerateSubscriptionId = "生成订阅ID"; 259 | public const string SubscriptionRecommendation = "建议在不同设备间使用相同订阅ID,以同步收藏"; 260 | public const string EmptySubscriptionWarning = "注意:空订阅ID也是合法的订阅ID"; 261 | public const string NewThreadCreatedSuccessfully = "发串大成功"; 262 | public const string NewReplyCreatedSuccessfully = "回复成功"; 263 | public const string ReasonOfReporting = "举报理由"; 264 | public const string ImageSavedToLocation = "图片已保存到"; 265 | public const string NoMoreReplies = "没有了,一条也没有了"; 266 | 267 | public const string CreateThread = "创建新串"; 268 | public const string CreateReply = "创建回复"; 269 | public const string RefreshThread = "刷新"; 270 | public const string AddToFavourites = "收藏"; 271 | public const string SearchThread = "搜索串"; 272 | public const string GotoThread = "跳转到串"; 273 | public const string PoOnlyTooltip = "只看PO"; 274 | public const string DeleteSubscription = "删除订阅"; 275 | public const string ReportThread = "举报该串"; 276 | public const string Close = "关闭"; 277 | public const string SaveScratch = "保存草稿"; 278 | public const string InsertEmoticon = "插入颜文字"; 279 | public const string InsertImage = "插入图片"; 280 | public const string RemoveImage = "删除图片"; 281 | public const string Watermark = "水印"; 282 | public const string Username = "名称"; 283 | public const string Email = "E-Mail"; 284 | public const string Title = "标题"; 285 | public const string Content = "正文"; 286 | public const string ImagePreview = "图片附件预览"; 287 | public const string SaveImage = "保存图片"; 288 | public const string GitHubRepo = "GitHub仓库"; 289 | public const string SponsorOnAiFaDian = "在爱发电上赞助我"; 290 | public const string Copy = "复制"; 291 | public const string GotoPage = "跳到指定页"; 292 | public const string GotoPagePopupTitle = "跳到页码"; 293 | } 294 | 295 | public static class ConfirmationMessage 296 | { 297 | public const string DeleteCookieConfirmation = "真的要删掉这块饼干吗?\n并不会真的碎饼哦,你随时还可以再添加进来~"; 298 | public const string GenerateNewSubscriptionIdConfirmationTitle = "确定要生成新的订阅ID吗?"; 299 | public const string GenerateNewSubscriptionIdConfirmationContent = "如果没有备份的话,你将会永久丢失当前的订阅ID!"; 300 | public const string KeepContentConfirmation = "要保留当前的内容吗?"; 301 | public const string LoadAllRepliesConfirmation = "确定要一次性载入所有回复吗?\n如果有很多回复的话,一次性全部加载会对服务器造成较大压力,确定要继续加载吗?"; 302 | } 303 | 304 | public static class ErrorMessage 305 | { 306 | public const string FileIsNotImage = "选择的文件不是图片"; 307 | public const string QrCodeDecodeFailed = "饼干二维码解析失败"; 308 | public const string CookieRequiredForThisForum = "浏览该板块需要拥有饼干"; 309 | public const string CookieRequiredForCreatingThread = "发串和发回复需要拥有饼干"; 310 | public const string ContentRequiredWhenNoImageAttached = "没有上传文件的时候,必须填写内容"; 311 | public const string LargeImageLoadFailed = "大图下载失败"; 312 | public const string InvalidConfigFile = "AppConfig.xml无效,请联系开发者"; 313 | public const string InvalidAiFaDianConfig = "爱发电参数无效,加载赞助者失败"; 314 | public const string AiFaDianApiError = "爱发电API返回了一个错误"; 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /XoW/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XoW 4 | { 5 | public class AppException : Exception 6 | { 7 | public AppException(string message) : base(message) { } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /XoW/GlobalState.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using Windows.UI; 4 | using Windows.UI.Xaml.Media; 5 | using XoW.Models; 6 | using XoW.Utils; 7 | using XoW.Views; 8 | 9 | namespace XoW 10 | { 11 | public static class GlobalState 12 | { 13 | /// 14 | /// 版名 -> (版ID, 权限级别) 15 | /// 目前已知权限级别2代表需要cookie 16 | /// 17 | internal static Dictionary ForumAndIdLookup = default; 18 | 19 | internal static string CurrentForumId = Constants.TimelineForumId; 20 | internal static string CurrentThreadId = default; 21 | internal static string CurrentThreadAuthorUserHash = default; 22 | internal static string CdnUrl; 23 | 24 | internal static string AiFaDianUsername; 25 | internal static string AiFaDianApiToken; 26 | 27 | internal static bool isPoOnly; 28 | 29 | internal static MainPage MainPageObjectReference; 30 | internal static LargeImageViewUserControl LargeImageViewObjectReference; 31 | 32 | internal static readonly ObservableCollection Cookies = new ObservableCollection(); 33 | internal static readonly ObservableCollection AiFaDianSponsoredUsers = new ObservableCollection(); 34 | 35 | internal static ObservableObject ObservableObject = new ObservableObject 36 | { 37 | BackgroundAndBorderColorBrush = new SolidColorBrush(Colors.LightGray), 38 | ListViewBackgroundColorBrush = new SolidColorBrush(Colors.White), 39 | CurrentCookie = ApplicationConfigurationHelper.GetCurrentCookie(), 40 | SubscriptionId = ApplicationConfigurationHelper.GetSubscriptionId(), 41 | ForumName = string.Empty, 42 | ThreadId = string.Empty 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /XoW/Models/AiFaDianSponsor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace XoW.Models 4 | { 5 | public class AiFaDianRequestBody 6 | { 7 | public string UserId { get; set; } 8 | public string Params { get; set; } 9 | public long Ts { get; set; } 10 | public string Sign { get; set; } 11 | } 12 | 13 | public class AiFaDianResponse 14 | { 15 | /// 16 | /// Error Code 17 | /// 18 | public int Ec { get; set; } 19 | 20 | /// 21 | /// Error Message 22 | /// 23 | public string Em { get; set; } 24 | 25 | /// 26 | /// Data 27 | /// 28 | public T Data { get; set; } 29 | } 30 | 31 | public class AiFaDianSponsor 32 | { 33 | public int TotalCount { get; set; } 34 | public int TotalPage { get; set; } 35 | public List List { get; set; } 36 | } 37 | 38 | public class AiFaDianSponsorList 39 | { 40 | public List SponsorPlans { get; set; } 41 | public AiFaDianSponsorPlan CurrentPlan { get; set; } 42 | public string AllSumAmount { get; set; } 43 | public long FirstPayTime { get; set; } 44 | public long LastPayTime { get; set; } 45 | public AiFaDianUser User { get; set; } 46 | } 47 | 48 | public class AiFaDianSponsorPlan 49 | { 50 | public string PlanId { get; set; } 51 | public int Rank { get; set; } 52 | public string UserId { get; set; } 53 | public int Status { get; set; } 54 | public string Name { get; set; } 55 | public string Pic { get; set; } 56 | public string Desc { get; set; } 57 | public string Price { get; set; } 58 | public long UpdateTime { get; set; } 59 | public int PayMonth { get; set; } 60 | public string ShowPrice { get; set; } 61 | public int Independent { get; set; } 62 | public int Permanent { get; set; } 63 | public int CanBuyHide { get; set; } 64 | public int NeedAddress { get; set; } 65 | public int ProductType { get; set; } 66 | public int SaleLimitCount { get; set; } 67 | public bool NeedInviteCode { get; set; } 68 | public long ExpireTime { get; set; } 69 | public List SkuProcessed { get; set; } 70 | public int RankType { get; set; } 71 | } 72 | 73 | public class AiFaDianUser 74 | { 75 | public string UserId { get; set; } 76 | public string Name { get; set; } 77 | public string Avatar { get; set; } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /XoW/Models/AnoBbsCookie.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XoW.Models 4 | { 5 | public class AnoBbsCookie : IEquatable 6 | { 7 | public string Cookie { get; set; } 8 | public string Name { get; set; } 9 | 10 | public bool Equals(AnoBbsCookie other) => other?.Cookie == Cookie && other?.Name == Name; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /XoW/Models/CdnUrl.cs: -------------------------------------------------------------------------------- 1 | namespace XoW.Models 2 | { 3 | public class CdnUrl 4 | { 5 | public string Url { get; set; } 6 | public double Rate { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /XoW/Models/ForumGroup.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace XoW.Models 5 | { 6 | /// 7 | /// 板块组,如“综合”,“二次元” 8 | /// 9 | public class ForumGroup 10 | { 11 | /// 12 | /// 该板块组的ID 13 | /// 14 | public int Id { get; set; } 15 | 16 | /// 17 | /// 服务器的排序值,越小优先级越高,若为-1则自动排序 18 | /// 19 | public int Sort { get; set; } 20 | 21 | /// 22 | /// 板块组名称 23 | /// 24 | public string Name { get; set; } 25 | 26 | /// 27 | /// 28 | public string Status { get; set; } 29 | 30 | /// 31 | /// 板块列表 32 | /// 33 | public IEnumerable Forums { get; set; } 34 | } 35 | 36 | /// 37 | /// 板块,如“时间线”,“综合版1” 38 | /// 39 | public class Forum 40 | { 41 | /// 42 | /// 板块ID 43 | /// 44 | public string Id { get; set; } 45 | 46 | /// 47 | /// 所属板块组ID 48 | /// 49 | public string FGroup { get; set; } 50 | 51 | /// 52 | /// 服务器的排序值,越小优先级越高,若为-1则自动排序 53 | /// 54 | public int Sort { get; set; } 55 | 56 | /// 57 | /// 板块名称 58 | /// 59 | public string Name { get; set; } 60 | 61 | /// 62 | /// 板块显示的名字,若该值不为空则显示该值(包含html) 63 | /// 64 | public string ShowName { get; set; } 65 | 66 | /// 67 | /// 68 | public string Msg { get; set; } 69 | 70 | /// 71 | /// 该板块发言间隔 72 | /// 73 | public int Interval { get; set; } 74 | 75 | [JsonProperty("thread_count")] 76 | public int threadCount { get; set; } 77 | 78 | [JsonProperty("permission_level")] 79 | public string permissionLevel { get; set; } 80 | 81 | [JsonProperty("forum_fuse_id")] 82 | public string forumFuseId { get; set; } 83 | public string CreatedAt { get; set; } 84 | public string UpdateAt { get; set; } 85 | 86 | /// 87 | /// 始终为n 88 | /// 89 | public string Status { get; set; } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /XoW/Models/ForumThread.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace XoW.Models 6 | { 7 | /// 8 | /// 串,也可以是回复,因为回复也是串 9 | /// 10 | public class ForumThread 11 | { 12 | /// 13 | /// 串ID 14 | /// 15 | public string Id { get; set; } 16 | 17 | /// 18 | /// 板块ID 19 | /// 20 | public string FId { get; set; } 21 | 22 | public int ReplyCount { get; set; } 23 | 24 | /// 25 | /// 图片相对地址 26 | /// 27 | public string Img { get; set; } 28 | 29 | /// 30 | /// 图片后缀 31 | /// 32 | public string Ext { get; set; } 33 | 34 | /// 35 | /// 发言时间 36 | /// 37 | public string Now { get; set; } 38 | 39 | /// 40 | /// 饼干 41 | /// 42 | [JsonProperty("user_hash")] 43 | public string UserHash { get; set; } 44 | 45 | /// 46 | /// 发言者名 47 | /// 48 | public string Name { get; set; } 49 | 50 | /// 51 | /// 串标题 52 | /// 53 | public string Title { get; set; } 54 | 55 | /// 56 | /// 串内容,HTML 57 | /// 58 | public string Content { get; set; } 59 | 60 | /// 61 | /// 是否sage 62 | /// 63 | public string Sage { get; set; } 64 | 65 | /// 66 | /// 是否为炫酷红名 67 | /// 68 | public string Admin { get; set; } 69 | public string Hide { get; set; } 70 | 71 | [JsonIgnore] 72 | public int RemainReplies { get; set; } 73 | 74 | [JsonIgnore] 75 | public List ContentParts { get; set; } 76 | 77 | public static void SplittingContentIntoList(ForumThread thread) 78 | { 79 | var contentParts = thread.Content.Split(Environment.NewLine); 80 | thread.ContentParts = new List(contentParts); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /XoW/Models/IncrementalLoadingSources.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.Toolkit.Collections; 7 | using Windows.UI.Xaml.Controls; 8 | using XoW.Services; 9 | using XoW.Utils; 10 | using XoW.Views; 11 | 12 | namespace XoW.Models 13 | { 14 | public abstract class IncrementalSourceWithPageNumber 15 | { 16 | private int _pageIndex; 17 | 18 | public int PageIndex => _pageIndex; 19 | 20 | public IncrementalSourceWithPageNumber() 21 | { 22 | _pageIndex = 1; 23 | } 24 | 25 | public IncrementalSourceWithPageNumber(int pageIndex) 26 | { 27 | _pageIndex = pageIndex; 28 | } 29 | 30 | public void IncreasePageIndex() => _pageIndex++; 31 | } 32 | 33 | public class TimelineForumThreadSource : IIncrementalSource 34 | { 35 | public async Task> GetPagedItemsAsync(int pageIndex, int pageSize, CancellationToken cancellationToken = default) 36 | { 37 | var threads = await AnoBbsApiClient.GetTimelineAsync(pageIndex + 1); 38 | var gridsInTheListView = await ComponentsBuilder.BuildGridForThread(threads, GlobalState.CdnUrl); 39 | 40 | return gridsInTheListView; 41 | } 42 | } 43 | 44 | public class NormalForumThreadSource : IIncrementalSource 45 | { 46 | public async Task> GetPagedItemsAsync(int pageIndex, int pageSize, CancellationToken cancellationToken = default) 47 | { 48 | var threads = await AnoBbsApiClient.GetThreadsAsync(GlobalState.CurrentForumId, pageIndex + 1); 49 | var gridsInTheListView = await ComponentsBuilder.BuildGridForThread(threads, GlobalState.CdnUrl); 50 | 51 | return gridsInTheListView; 52 | } 53 | } 54 | 55 | public class ThreadReplySource : IncrementalSourceWithPageNumber, IIncrementalSource 56 | { 57 | public ThreadReplySource() : base() 58 | { 59 | 60 | } 61 | 62 | public ThreadReplySource(int pageIndex) : base(pageIndex) 63 | { 64 | 65 | } 66 | 67 | public async Task> GetPagedItemsAsync(int pageIndex, int pageSize, CancellationToken cancellationToken = default) 68 | { 69 | var replies = await AnoBbsApiClient.GetRepliesAsync(GlobalState.CurrentThreadId, PageIndex); 70 | var threadId = replies.Id; 71 | var threadAuthorUserHash = replies.UserHash; 72 | 73 | GlobalState.ObservableObject.ThreadId = threadId; 74 | GlobalState.CurrentThreadAuthorUserHash = threadAuthorUserHash; 75 | 76 | var grids = PageIndex == 1 77 | ? await ComponentsBuilder.BuildGridForReply(replies, GlobalState.CdnUrl) 78 | : await ComponentsBuilder.BuildGridForOnlyReplies(replies.Replies.Where(reply => reply.UserHash != "Tips").ToList(), GlobalState.CdnUrl); 79 | 80 | if (!grids.Any()) 81 | { 82 | await new NotificationContentDialog(false, ComponentContent.NoMoreReplies).ShowAsync(); 83 | return grids; 84 | } 85 | 86 | GlobalState.ObservableObject.CurrentPageNumber = PageIndex; 87 | IncreasePageIndex(); 88 | return grids; 89 | } 90 | } 91 | 92 | public class PoOnlyThreadReplySource : IncrementalSourceWithPageNumber, IIncrementalSource 93 | { 94 | public PoOnlyThreadReplySource() : base() 95 | { 96 | 97 | } 98 | 99 | public PoOnlyThreadReplySource(int pageIndex) : base(pageIndex) 100 | { 101 | 102 | } 103 | 104 | public async Task> GetPagedItemsAsync(int pageIndex, int pageSize, CancellationToken cancellationToken = default) 105 | { 106 | var actualPageIndex = PageIndex == 0 ? 1 : PageIndex; 107 | var replies = await AnoBbsApiClient.GetPoOnlyRepliesAsync(GlobalState.CurrentThreadId, actualPageIndex); 108 | var grids = actualPageIndex == 1 109 | ? await ComponentsBuilder.BuildGridForReply(replies, GlobalState.CdnUrl) 110 | : await ComponentsBuilder.BuildGridForOnlyReplies(replies.Replies.Where(reply => reply.UserHash != "Tips").ToList(), GlobalState.CdnUrl); 111 | 112 | if (!grids.Any()) 113 | { 114 | await new NotificationContentDialog(false, ComponentContent.NoMoreReplies).ShowAsync(); 115 | return grids; 116 | } 117 | 118 | GlobalState.ObservableObject.CurrentPageNumber = PageIndex; 119 | IncreasePageIndex(); 120 | 121 | return grids; 122 | } 123 | } 124 | 125 | public class SubscriptionSource : IncrementalSourceWithPageNumber, IIncrementalSource 126 | { 127 | public async Task> GetPagedItemsAsync(int pageIndex, int pageSize, CancellationToken cancellationToken = default) 128 | { 129 | var actualPageIndex = pageIndex + 1; 130 | var subscriptions = await AnoBbsApiClient.GetSubscriptionsAsync(GlobalState.ObservableObject.SubscriptionId, actualPageIndex); 131 | var grids = await ComponentsBuilder.BuildGrids(subscriptions, GlobalState.CdnUrl, isForSubscription: true); 132 | 133 | return grids; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /XoW/Models/ObservableObject.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.CompilerServices; 3 | using Windows.UI.Xaml.Media; 4 | 5 | namespace XoW.Models 6 | { 7 | public class ObservableObject : INotifyPropertyChanged 8 | { 9 | 10 | private SolidColorBrush _backgroundAndBorderColorBrush; 11 | private string _currentCookie; 12 | private string _forumName; 13 | private SolidColorBrush _listViewBackgroundColorBrush; 14 | private string _subscriptionId; 15 | private string _threadId; 16 | private int _currentPageNumber; 17 | private int _totalPageNumber; 18 | 19 | public SolidColorBrush BackgroundAndBorderColorBrush 20 | { 21 | get => _backgroundAndBorderColorBrush; 22 | set 23 | { 24 | if (value != _backgroundAndBorderColorBrush) 25 | { 26 | _backgroundAndBorderColorBrush = value; 27 | OnPropertyChanged(); 28 | } 29 | } 30 | } 31 | 32 | public SolidColorBrush ListViewBackgroundColorBrush 33 | { 34 | get => _listViewBackgroundColorBrush; 35 | set 36 | { 37 | if (value != _listViewBackgroundColorBrush) 38 | { 39 | _listViewBackgroundColorBrush = value; 40 | OnPropertyChanged(); 41 | } 42 | } 43 | } 44 | 45 | public string CurrentCookie 46 | { 47 | get => _currentCookie; 48 | set 49 | { 50 | if (value != _currentCookie) 51 | { 52 | _currentCookie = value; 53 | OnPropertyChanged(); 54 | } 55 | } 56 | } 57 | 58 | public string ThreadId 59 | { 60 | get => _threadId; 61 | set 62 | { 63 | if (_threadId != value) 64 | { 65 | _threadId = value; 66 | OnPropertyChanged(); 67 | } 68 | } 69 | } 70 | 71 | public string ForumName 72 | { 73 | get => _forumName; 74 | set 75 | { 76 | if (_forumName != value) 77 | { 78 | _forumName = value; 79 | OnPropertyChanged(); 80 | } 81 | } 82 | } 83 | 84 | public string SubscriptionId 85 | { 86 | get => _subscriptionId; 87 | set 88 | { 89 | if (_subscriptionId != value) 90 | { 91 | _subscriptionId = value; 92 | OnPropertyChanged(); 93 | } 94 | } 95 | } 96 | 97 | public int CurrentPageNumber 98 | { 99 | get => _currentPageNumber; 100 | set 101 | { 102 | if (_currentPageNumber != value) 103 | { 104 | _currentPageNumber = value; 105 | OnPropertyChanged(); 106 | } 107 | } 108 | } 109 | 110 | public int TotalPageNumber 111 | { 112 | get => _totalPageNumber; 113 | set 114 | { 115 | _totalPageNumber = value; 116 | OnPropertyChanged(); 117 | } 118 | } 119 | 120 | public event PropertyChangedEventHandler PropertyChanged; 121 | 122 | private void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /XoW/Models/ThreadDataContext.cs: -------------------------------------------------------------------------------- 1 | namespace XoW.Models 2 | { 3 | public class ThreadDataContext 4 | { 5 | public string ThreadId { get; set; } 6 | public string ThreadAuthorUserHash { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /XoW/Models/ThreadReply.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace XoW.Models 4 | { 5 | public class ThreadReply : ForumThread 6 | { 7 | public List Replies { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /XoW/Models/ThreadSubscription.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace XoW.Models 4 | { 5 | public class ThreadSubscription : ForumThread 6 | { 7 | [JsonProperty("user_id")] 8 | public string UserId { get; set; } 9 | 10 | #pragma warning disable CS0108 11 | // 隐藏 ForumThread 类中的 ReplyCount 属性 12 | // 因为在这里需要重新指定 JsonProperty 13 | [JsonProperty("reply_count")] 14 | public int ReplyCount { get; set; } 15 | #pragma warning restore CS0108 16 | 17 | [JsonProperty("recent_replies")] 18 | public string RecentReplies { get; set; } 19 | 20 | public string Category { get; set; } 21 | 22 | [JsonProperty("file_id")] 23 | public string FileId { get; set; } 24 | 25 | public string Email { get; set; } 26 | 27 | public string Status { get; set; } 28 | 29 | public string Po { get; set; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /XoW/Package.appxmanifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | XoW 7 | boris1993 8 | Assets\StoreLogo.png 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /XoW/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Resources; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("XoW")] 9 | [assembly: AssemblyDescription("X岛 on Windows")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("boris1993")] 12 | [assembly: AssemblyProduct("XoW")] 13 | [assembly: AssemblyCopyright("Copyright © 2022")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Version information for an assembly consists of the following four values: 18 | // 19 | // Major Version 20 | // Minor Version 21 | // Build Number 22 | // Revision 23 | // 24 | // You can specify all the values or you can default the Build and Revision Numbers 25 | // by using the '*' as shown below: 26 | // [assembly: AssemblyVersion("1.0.*")] 27 | [assembly: AssemblyVersion("1.0.0.0")] 28 | [assembly: AssemblyFileVersion("1.0.0.0")] 29 | [assembly: ComVisible(false)] 30 | [assembly: NeutralResourcesLanguage("zh-Hans")] 31 | -------------------------------------------------------------------------------- /XoW/Properties/Default.rd.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /XoW/Services/AiFaDianApiClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Serialization; 8 | using Windows.Web.Http; 9 | using XoW.Models; 10 | using XoW.Utils; 11 | using UnicodeEncoding = Windows.Storage.Streams.UnicodeEncoding; 12 | 13 | namespace XoW.Services 14 | { 15 | public static class AiFaDianApiClient 16 | { 17 | private const string QuerySponsor = "https://afdian.net/api/open/query-sponsor"; 18 | 19 | private static readonly JsonSerializerSettings DefaultSerializerSettings = new JsonSerializerSettings 20 | { 21 | ContractResolver = new DefaultContractResolver 22 | { 23 | NamingStrategy = new SnakeCaseNamingStrategy() 24 | } 25 | }; 26 | 27 | public static async Task GetSponsorList(int page = 1) 28 | { 29 | var sponsors = await LoadSponsors(); 30 | while (++page <= sponsors.TotalPage) 31 | { 32 | var nextPageSponsors = await LoadSponsors(page); 33 | sponsors.List.AddRange(nextPageSponsors.List); 34 | } 35 | 36 | return sponsors; 37 | } 38 | 39 | private static async Task LoadSponsors(int page = 1) 40 | { 41 | var userId = GlobalState.AiFaDianUsername; 42 | var token = GlobalState.AiFaDianApiToken; 43 | if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(token)) 44 | { 45 | throw new AppException(ErrorMessage.InvalidAiFaDianConfig); 46 | } 47 | 48 | var aiFaDianRequestParams = new Dictionary 49 | { 50 | {"token", token}, 51 | {"page", page.ToString()} 52 | }; 53 | 54 | var paramsJsonString = JsonConvert.SerializeObject(aiFaDianRequestParams, DefaultSerializerSettings); 55 | var currentTimestamp = new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds(); 56 | 57 | var stringForCalculatingMd5 = $"{token}params{paramsJsonString}ts{currentTimestamp}user_id{userId}"; 58 | var signature = CalculateMd5(stringForCalculatingMd5); 59 | 60 | var aiFaDianRequestBody = new AiFaDianRequestBody 61 | { 62 | UserId = userId, 63 | Params = paramsJsonString, 64 | Ts = currentTimestamp, 65 | Sign = signature 66 | }; 67 | 68 | using var requestBodyContent = new HttpStringContent(JsonConvert.SerializeObject(aiFaDianRequestBody, DefaultSerializerSettings), UnicodeEncoding.Utf8, "application/json"); 69 | 70 | var httpClient = HttpClientService.GetHttpClientForThirdPartyInstance(); 71 | var uri = new Uri(QuerySponsor); 72 | var response = await httpClient.PostAsync(uri, requestBodyContent); 73 | response.EnsureSuccessStatusCode(); 74 | var responseString = await response.Content.ReadAsStringAsync(); 75 | 76 | var aiFaDianResponse = JsonConvert.DeserializeObject>(responseString); 77 | if (aiFaDianResponse.Ec != 200) 78 | { 79 | throw new AppException($"{ErrorMessage.AiFaDianApiError}\n{aiFaDianResponse.Em}"); 80 | } 81 | 82 | var aiFaDianSponsor = aiFaDianResponse.Data; 83 | 84 | return aiFaDianSponsor; 85 | } 86 | 87 | private static string CalculateMd5(string input) 88 | { 89 | using var md5 = MD5.Create(); 90 | var inputBytes = Encoding.ASCII.GetBytes(input); 91 | var outputBytes = md5.ComputeHash(inputBytes); 92 | 93 | return outputBytes.ToHexString(); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /XoW/Services/AnoBbsApiClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.Web; 6 | using Microsoft.Net.Http.Headers; 7 | using Newtonsoft.Json; 8 | using Newtonsoft.Json.Linq; 9 | using Windows.Storage; 10 | using Windows.Web.Http; 11 | using Windows.Web.Http.Headers; 12 | using XoW.Models; 13 | 14 | namespace XoW.Services 15 | { 16 | public static class AnoBbsApiClient 17 | { 18 | /// 19 | /// 值班室版面ID 20 | /// 21 | private const string ForumIdDutyRoom = "18"; 22 | 23 | /// 24 | /// 每页回复数量 25 | /// 26 | private static double _pageSize = 19; 27 | 28 | public static async Task> GetCdnAsync() 29 | { 30 | var cdnList = await GetResponseWithType>(Url.GetCdn); 31 | cdnList = cdnList.OrderBy(entry => entry.Rate).ToList(); 32 | 33 | return cdnList; 34 | } 35 | 36 | public static async Task> GetForumGroupsAsync() 37 | { 38 | var forumGroups = await GetResponseWithType>(Url.GetForums); 39 | return forumGroups; 40 | } 41 | 42 | public static async Task> GetTimelineAsync(int pageId = 1) 43 | { 44 | var uriBuilder = new UriBuilder(Url.GetTimeline); 45 | 46 | var query = HttpUtility.ParseQueryString(uriBuilder.Query); 47 | query[QueryParams.QueryParamPageId] = pageId.ToString(); 48 | uriBuilder.Query = query.ToString(); 49 | 50 | var threads = await GetResponseWithType>(uriBuilder.ToString()); 51 | 52 | return threads; 53 | } 54 | 55 | public static async Task> GetThreadsAsync(string forumId, int pageId = 1) 56 | { 57 | var uriBuilder = new UriBuilder(Url.GetThreads); 58 | 59 | var query = HttpUtility.ParseQueryString(uriBuilder.Query); 60 | query[QueryParams.QueryParamId] = forumId; 61 | query[QueryParams.QueryParamPageId] = pageId.ToString(); 62 | uriBuilder.Query = query.ToString(); 63 | 64 | var threads = await GetResponseWithType>(uriBuilder.ToString()); 65 | 66 | return threads; 67 | } 68 | 69 | public static async Task GetRepliesAsync(string threadId, int pageId = 1) 70 | { 71 | var uriBuilder = new UriBuilder(Url.GetReplies); 72 | 73 | var query = HttpUtility.ParseQueryString(uriBuilder.Query); 74 | query[QueryParams.QueryParamId] = threadId; 75 | query[QueryParams.QueryParamPageId] = pageId.ToString(); 76 | uriBuilder.Query = query.ToString(); 77 | 78 | var reply = await GetResponseWithType(uriBuilder.ToString()); 79 | 80 | GlobalState.ObservableObject.TotalPageNumber = (int)Math.Ceiling(reply.ReplyCount / _pageSize); 81 | 82 | return reply; 83 | } 84 | 85 | public static async Task GetPoOnlyRepliesAsync(string threadId, int pageId = 1) 86 | { 87 | var uriBuilder = new UriBuilder(Url.GetPoOnlyReplies); 88 | 89 | var query = HttpUtility.ParseQueryString(uriBuilder.Query); 90 | query[QueryParams.QueryParamId] = threadId; 91 | query[QueryParams.QueryParamPageId] = pageId.ToString(); 92 | uriBuilder.Query = query.ToString(); 93 | 94 | var reply = await GetResponseWithType(uriBuilder.ToString()); 95 | GlobalState.ObservableObject.TotalPageNumber = (int)Math.Ceiling(reply.ReplyCount / _pageSize); 96 | 97 | return reply; 98 | } 99 | 100 | public static async Task> GetSubscriptionsAsync(string subscriptionId, int pageId = 1) 101 | { 102 | var uriBuilder = new UriBuilder(Url.GetSubscription); 103 | 104 | var query = HttpUtility.ParseQueryString(uriBuilder.Query); 105 | query[QueryParams.QueryParamUuid] = subscriptionId; 106 | query[QueryParams.QueryParamPageId] = pageId.ToString(); 107 | uriBuilder.Query = query.ToString(); 108 | 109 | var subscriptions = await GetResponseWithType>(uriBuilder.ToString()); 110 | 111 | return subscriptions; 112 | } 113 | 114 | public static async Task AddSubscriptionAsync(string subscriptionId, string tid) 115 | { 116 | var uriBuilder = new UriBuilder(Url.AddSubscription); 117 | 118 | var query = HttpUtility.ParseQueryString(uriBuilder.Query); 119 | query[QueryParams.QueryParamUuid] = subscriptionId; 120 | query[QueryParams.QueryParamTid] = tid; 121 | uriBuilder.Query = query.ToString(); 122 | 123 | return await GetResponseWithType(uriBuilder.ToString()); 124 | } 125 | 126 | public static async Task DeleteSubscriptionAsync(string subscriptionId, string tid) 127 | { 128 | var uriBuilder = new UriBuilder(Url.DeleteSubscription); 129 | 130 | var query = HttpUtility.ParseQueryString(uriBuilder.Query); 131 | query[QueryParams.QueryParamUuid] = subscriptionId; 132 | query[QueryParams.QueryParamTid] = tid; 133 | uriBuilder.Query = query.ToString(); 134 | 135 | return await GetResponseWithType(uriBuilder.ToString()); 136 | } 137 | 138 | /// 139 | /// 搜索暂未开放,先留一个stub在这里,开放后再适配 140 | /// 141 | /// 142 | /// 143 | /// 144 | public static async Task SearchThread(string keyword, string page = "1") 145 | { 146 | var uriBuilder = new UriBuilder(Url.Search); 147 | 148 | var query = HttpUtility.ParseQueryString(uriBuilder.Query); 149 | query[QueryParams.QueryParamKeyword] = keyword; 150 | query[QueryParams.QueryParamPageId] = page; 151 | uriBuilder.Query = query.ToString(); 152 | 153 | return await GetResponseWithType(uriBuilder.ToString()); 154 | } 155 | 156 | public static async Task PostNewReport(string reportThreadContent) => 157 | await CreateNewThread(ForumIdDutyRoom, 158 | null, 159 | null, 160 | null, 161 | reportThreadContent, 162 | "0", 163 | GlobalState.Cookies.Single(cookie => cookie.Name == GlobalState.ObservableObject.CurrentCookie), 164 | null); 165 | 166 | public static async Task CreateNewThread(string fid, string name, string email, string title, string content, string water, AnoBbsCookie cookie, StorageFile image) 167 | { 168 | var uri = new Uri(Url.CreateNewThread); 169 | 170 | using var requestBody = new HttpMultipartFormDataContent(); 171 | requestBody.Add(new HttpStringContent(fid), RequestBodyParamName.FId); 172 | requestBody.Add(new HttpStringContent(content), RequestBodyParamName.Content); 173 | requestBody.Add(new HttpStringContent(water), RequestBodyParamName.Water); 174 | 175 | if (!string.IsNullOrWhiteSpace(name)) 176 | { 177 | requestBody.Add(new HttpStringContent(name), RequestBodyParamName.Username); 178 | } 179 | 180 | if (!string.IsNullOrWhiteSpace(email)) 181 | { 182 | requestBody.Add(new HttpStringContent(email), RequestBodyParamName.EMail); 183 | } 184 | 185 | if (!string.IsNullOrWhiteSpace(title)) 186 | { 187 | requestBody.Add(new HttpStringContent(title), RequestBodyParamName.Title); 188 | } 189 | 190 | if (image != null) 191 | { 192 | var imageStream = await image.OpenReadAsync(); 193 | var fileStreamContent = new HttpStreamContent(imageStream); 194 | fileStreamContent.Headers.Add(HeaderNames.ContentType, imageStream.ContentType); 195 | requestBody.Add(fileStreamContent, RequestBodyParamName.Image, $"/{image.Path.Replace('\\', '/')}"); 196 | } 197 | 198 | var httpClient = HttpClientService.GetHttpClientInstance(); 199 | 200 | var defaultCookie = httpClient.DefaultRequestHeaders.Cookie.Single(); 201 | httpClient.DefaultRequestHeaders.Cookie.Clear(); 202 | httpClient.DefaultRequestHeaders.Cookie.Add(new HttpCookiePairHeaderValue(Constants.CookieNameUserHash) 203 | { 204 | Value = cookie.Cookie 205 | }); 206 | 207 | var response = await httpClient.PostAsync(uri, requestBody); 208 | response.EnsureSuccessStatusCode(); 209 | 210 | httpClient.DefaultRequestHeaders.Cookie.Clear(); 211 | httpClient.DefaultRequestHeaders.Cookie.Add(defaultCookie); 212 | } 213 | 214 | public static async Task CreateNewReply(string resto, string name, string email, string title, string content, string water, AnoBbsCookie cookie, StorageFile image) 215 | { 216 | var uri = new Uri(Url.CreateNewReply); 217 | 218 | using var requestBody = new HttpMultipartFormDataContent(); 219 | requestBody.Add(new HttpStringContent(resto), RequestBodyParamName.Resto); 220 | requestBody.Add(new HttpStringContent(content), RequestBodyParamName.Content); 221 | requestBody.Add(new HttpStringContent(water), RequestBodyParamName.Water); 222 | 223 | if (!string.IsNullOrWhiteSpace(name)) 224 | { 225 | requestBody.Add(new HttpStringContent(name), RequestBodyParamName.Username); 226 | } 227 | 228 | if (!string.IsNullOrWhiteSpace(email)) 229 | { 230 | requestBody.Add(new HttpStringContent(email), RequestBodyParamName.EMail); 231 | } 232 | 233 | if (!string.IsNullOrWhiteSpace(title)) 234 | { 235 | requestBody.Add(new HttpStringContent(title), RequestBodyParamName.Title); 236 | } 237 | 238 | if (image != null) 239 | { 240 | var imageStream = await image.OpenReadAsync(); 241 | var fileStreamContent = new HttpStreamContent(imageStream); 242 | fileStreamContent.Headers.Add(HeaderNames.ContentType, imageStream.ContentType); 243 | requestBody.Add(fileStreamContent, RequestBodyParamName.Image, $"/{image.Path.Replace('\\', '/')}"); 244 | } 245 | 246 | var httpClient = HttpClientService.GetHttpClientInstance(); 247 | 248 | var defaultCookie = httpClient.DefaultRequestHeaders.Cookie.Single(); 249 | httpClient.DefaultRequestHeaders.Cookie.Clear(); 250 | httpClient.DefaultRequestHeaders.Cookie.Add(new HttpCookiePairHeaderValue(Constants.CookieNameUserHash) 251 | { 252 | Value = cookie.Cookie 253 | }); 254 | 255 | var response = await httpClient.PostAsync(uri, requestBody); 256 | response.EnsureSuccessStatusCode(); 257 | 258 | httpClient.DefaultRequestHeaders.Cookie.Clear(); 259 | httpClient.DefaultRequestHeaders.Cookie.Add(defaultCookie); 260 | } 261 | 262 | public static async Task GetReferencedThreadById(string threadId) 263 | { 264 | var uriBuilder = new UriBuilder(Url.GetThreadReference); 265 | 266 | var query = HttpUtility.ParseQueryString(uriBuilder.Query); 267 | query[QueryParams.QueryParamId] = threadId; 268 | uriBuilder.Query = query.ToString(); 269 | 270 | var httpClient = HttpClientService.GetHttpClientInstance(); 271 | 272 | var uri = new Uri(uriBuilder.ToString()); 273 | var response = await httpClient.GetAsync(uri); 274 | response.EnsureSuccessStatusCode(); 275 | 276 | var responseString = await response.Content.ReadAsStringAsync(); 277 | return responseString; 278 | } 279 | 280 | private static async Task GetResponseWithType(string url) 281 | { 282 | var httpClient = HttpClientService.GetHttpClientInstance(); 283 | 284 | var uri = new Uri(url); 285 | var response = await httpClient.GetAsync(uri); 286 | response.EnsureSuccessStatusCode(); 287 | 288 | var responseString = await response.Content.ReadAsStringAsync(); 289 | HandlePotentialErrorMessage(responseString); 290 | 291 | try 292 | { 293 | var returnObject = JsonConvert.DeserializeObject(responseString); 294 | return returnObject; 295 | } 296 | catch (JsonSerializationException) 297 | { 298 | var errorMessage = JsonConvert.DeserializeObject(responseString); 299 | throw new AppException(errorMessage); 300 | } 301 | } 302 | 303 | // ReSharper disable InvertIf 304 | private static void HandlePotentialErrorMessage(string responseString) 305 | { 306 | var responseJsonObject = JToken.Parse(responseString); 307 | if (responseJsonObject.SelectToken("success") != null && !responseJsonObject.Value("success")) 308 | { 309 | var errorMessage = responseJsonObject.SelectToken("error")?.ToString() ?? responseString; 310 | throw new AppException(errorMessage); 311 | } 312 | 313 | if (responseJsonObject.SelectToken("msg") != null) 314 | { 315 | var errorMessage = responseJsonObject.SelectToken("msg")?.ToString() ?? responseString; 316 | throw new AppException(errorMessage); 317 | } 318 | } 319 | } 320 | 321 | public static class QueryParams 322 | { 323 | public const string QueryParamId = "id"; 324 | public const string QueryParamUuid = "uuid"; 325 | public const string QueryParamPageId = "page"; 326 | public const string QueryParamTid = "tid"; 327 | public const string QueryParamKeyword = "q"; 328 | } 329 | 330 | public static class RequestBodyParamName 331 | { 332 | public const string FId = "fid"; 333 | public const string Resto = "resto"; 334 | public const string Username = "name"; 335 | public const string EMail = "email"; 336 | public const string Title = "title"; 337 | public const string Content = "content"; 338 | public const string Water = "water"; 339 | public const string Image = "image"; 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /XoW/Services/ConfigurationManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Windows.Data.Xml.Dom; 4 | using Windows.Storage; 5 | 6 | namespace XoW.Services 7 | { 8 | public static class ConfigurationManager 9 | { 10 | private const string ConfigFileName = "AppConfig.xml"; 11 | 12 | public static async Task LoadAppConfig() 13 | { 14 | var configFileUri = new Uri($"ms-appx:///{ConfigFileName}"); 15 | var configFile = await StorageFile.GetFileFromApplicationUriAsync(configFileUri); 16 | var configFileXmlDocument = await XmlDocument.LoadFromFileAsync(configFile); 17 | 18 | LoadAiFaDianToken(configFileXmlDocument); 19 | } 20 | 21 | private static void LoadAiFaDianToken(XmlDocument rootDocument) 22 | { 23 | var aiFaDianSectionNode = rootDocument.DocumentElement.SelectSingleNode("./aifadian"); 24 | if (aiFaDianSectionNode == null) 25 | { 26 | return; 27 | } 28 | 29 | var aiFaDianUsernameNode = aiFaDianSectionNode.SelectSingleNode("./add[@key='userId']/@value"); 30 | var aiFaDianTokenNode = aiFaDianSectionNode.SelectSingleNode("./add[@key='token']/@value"); 31 | GlobalState.AiFaDianUsername = aiFaDianUsernameNode?.NodeValue.ToString() ?? String.Empty; 32 | GlobalState.AiFaDianApiToken = aiFaDianTokenNode?.NodeValue.ToString() ?? String.Empty; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /XoW/Services/Converter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Linq; 4 | using Windows.UI.Xaml; 5 | using Windows.UI.Xaml.Data; 6 | using XoW.Models; 7 | 8 | namespace XoW.Services 9 | { 10 | public class CookieNameConverter : IValueConverter 11 | { 12 | public object Convert(object value, Type targetType, object parameter, string language) => 13 | string.IsNullOrEmpty((string)value) 14 | ? Constants.NoCookieSelected 15 | : value.ToString(); 16 | 17 | public object ConvertBack(object value, Type targetType, object parameter, string language) => null; 18 | } 19 | 20 | public class SetButtonStateByContentExistenceConverter : IValueConverter 21 | { 22 | public object Convert(object value, Type targetType, object parameter, string language) => !string.IsNullOrWhiteSpace(value.ToString()); 23 | 24 | public object ConvertBack(object value, Type targetType, object parameter, string language) => null; 25 | } 26 | 27 | public class CookieNameToObjectConverter : IValueConverter 28 | { 29 | public object Convert(object value, Type targetType, object parameter, string language) => GlobalState.Cookies.SingleOrDefault(cookie => cookie.Name == GlobalState.ObservableObject.CurrentCookie); 30 | 31 | 32 | public object ConvertBack(object value, Type targetType, object parameter, string language) => ((AnoBbsCookie)value).Name; 33 | } 34 | 35 | public class ThreadIdConverter : IValueConverter 36 | { 37 | public object Convert(object value, Type targetType, object parameter, string language) => $"No.{value}"; 38 | 39 | public object ConvertBack(object value, Type targetType, object parameter, string language) => null; 40 | } 41 | 42 | public class CollapseItemWhenListIsEmptyConverter : IValueConverter 43 | { 44 | public object Convert(object value, Type targetType, object parameter, string language) 45 | { 46 | if (value is IList list) 47 | { 48 | return list.Count == 0 49 | ? Visibility.Collapsed 50 | : Visibility.Visible; 51 | } 52 | 53 | return Visibility.Visible; 54 | } 55 | 56 | public object ConvertBack(object value, Type targetType, object parameter, string language) => null; 57 | } 58 | 59 | public class ShowItemWhenListIsEmptyConverter : IValueConverter 60 | { 61 | public object Convert(object value, Type targetType, object parameter, string language) 62 | { 63 | if (value is IList list) 64 | { 65 | return list.Count != 0 66 | ? Visibility.Collapsed 67 | : Visibility.Visible; 68 | } 69 | 70 | return Visibility.Visible; 71 | } 72 | 73 | public object ConvertBack(object value, Type targetType, object parameter, string language) => null; 74 | } 75 | 76 | public class AiFaDianAvatarToThumbConverter : IValueConverter 77 | { 78 | public object Convert(object value, Type targetType, object parameter, string language) => new Uri($"{value}?imageView2/1/w/40/h/40"); 79 | public object ConvertBack(object value, Type targetType, object parameter, string language) => null; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /XoW/Services/HtmlParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using System.Threading.Tasks; 6 | using HtmlAgilityPack; 7 | using Microsoft.Toolkit.Uwp.Helpers; 8 | using Windows.UI; 9 | using Windows.UI.Text; 10 | using Windows.UI.Xaml.Controls; 11 | using Windows.UI.Xaml.Documents; 12 | using Windows.UI.Xaml.Media; 13 | using XoW.Utils; 14 | 15 | namespace XoW.Services 16 | { 17 | public static class HtmlParser 18 | { 19 | private const string XPathTextNodeAnywhere = "//text()"; 20 | private const string XPathBrNode = "br"; 21 | private const string XPathBrNodeAnywhere = "//br"; 22 | private const string XPathDivWithClassThreadsContent = "//div[@class='h-threads-content']"; 23 | private const string AttributeStyle = "style"; 24 | private const string AttributeStyleParamColor = "color"; 25 | private const string AttributeStyleParamFontWeight = "font-weight"; 26 | private const string XPathANode = "a"; 27 | private const string AttributeHyperlink = "href"; 28 | private const string EmailToScheme = "mailto:"; 29 | 30 | private static readonly Regex ThreadReferenceRegex = new Regex(@"^>> *(No\.)*\d+$", RegexOptions.Compiled); 31 | 32 | public static async Task> ParseHtmlIntoTextBlocks(string htmlString, bool textSelectionEnabled = false) 33 | { 34 | var rootHtmlDoc = new HtmlDocument(); 35 | rootHtmlDoc.LoadHtml(htmlString); 36 | var firstTextNode = rootHtmlDoc.DocumentNode.SelectNodes(XPathTextNodeAnywhere).FirstOrDefault(); 37 | 38 | var shouldBoldForAllTextBlocks = false; 39 | Color? textBlockGlobalColor = null; 40 | GetGlobalStyleForAllTextBlocks(firstTextNode, ref shouldBoldForAllTextBlocks, ref textBlockGlobalColor); 41 | 42 | var textBlocksForContents = new List(); 43 | var linesOfHtmlString = htmlString.Split(Environment.NewLine).ToList(); 44 | 45 | // 一部分换行符不标准,只有\n或\r 46 | // 在这里单独处理,将这些带有非标准换行符的行也正确分割 47 | for (var i = 0; i < linesOfHtmlString.Count; i++) 48 | { 49 | var line = linesOfHtmlString[i]; 50 | if (line.Contains(Environment.NewLine)) 51 | { 52 | continue; 53 | } 54 | 55 | if (line.Contains("\r")) 56 | { 57 | var linesSeparated = line.Split("\r"); 58 | linesOfHtmlString.RemoveAt(i); 59 | 60 | var insertLocation = i; 61 | foreach (var newLine in linesSeparated) 62 | { 63 | linesOfHtmlString.Insert(insertLocation, newLine); 64 | insertLocation++; 65 | } 66 | i = i + linesOfHtmlString.Count(); 67 | } 68 | 69 | if (line.Contains("\n")) 70 | { 71 | var linesSeparated = line.Split("\n"); 72 | linesOfHtmlString.RemoveAt(i); 73 | 74 | var insertLocation = i; 75 | foreach (var newLine in linesSeparated) 76 | { 77 | linesOfHtmlString.Insert(insertLocation, newLine); 78 | insertLocation++; 79 | } 80 | i = i + linesOfHtmlString.Count(); 81 | } 82 | } 83 | 84 | foreach (var line in linesOfHtmlString) 85 | { 86 | var htmlDoc = new HtmlDocument(); 87 | htmlDoc.LoadHtml(line); 88 | 89 | // 仅删除每行文字末尾的换行符 90 | // 如果只有换行符,那么就保留 91 | // 以保证显示效果与网页一致 92 | if (htmlDoc.DocumentNode.ChildNodes.Count > 1 && htmlDoc.DocumentNode.Descendants("br").Any()) 93 | { 94 | foreach (var node in htmlDoc.DocumentNode.SelectNodes(XPathBrNode)) 95 | { 96 | node.Remove(); 97 | } 98 | } 99 | 100 | var content = htmlDoc.DocumentNode.InnerText; 101 | var deEntitizeContent = HtmlEntity.DeEntitize(content.Trim()); 102 | 103 | TextBlock textBlock; 104 | var threadReferenceRegexMatch = ThreadReferenceRegex.Match(deEntitizeContent); 105 | if (threadReferenceRegexMatch.Success) 106 | { 107 | var contentForThisTextBlock = deEntitizeContent; 108 | 109 | var referencedThreadId = deEntitizeContent.Replace(">", "").Replace("No.", "").Trim(); 110 | var referencedContent = await GetReferencedThread(referencedThreadId); 111 | contentForThisTextBlock += $"\n{referencedContent}\n"; 112 | 113 | textBlock = ComponentsBuilder.CreateTextBlock(contentForThisTextBlock, Colors.DarkGreen, textSelectionEnabled: textSelectionEnabled); 114 | } 115 | else 116 | { 117 | textBlock = ComponentsBuilder.CreateTextBlock(deEntitizeContent, textSelectionEnabled: textSelectionEnabled); 118 | } 119 | SetEmailHyperLinkInTextBlock(htmlDoc.DocumentNode, textBlock); 120 | 121 | if (shouldBoldForAllTextBlocks) 122 | { 123 | textBlock.FontWeight = FontWeights.Bold; 124 | } 125 | 126 | if (textBlockGlobalColor != null) 127 | { 128 | textBlock.Foreground = new SolidColorBrush((Color)textBlockGlobalColor); 129 | } 130 | 131 | textBlocksForContents.Add(textBlock); 132 | } 133 | 134 | return textBlocksForContents; 135 | } 136 | 137 | private static void GetGlobalStyleForAllTextBlocks(HtmlNode parentNodeOfTheFirstTextNode, ref bool shouldBold, ref Color? textBlockGlobalColor) 138 | { 139 | // 先直接递归到顶层节点 140 | // 然后逐层在Span节点中寻找style属性 141 | // 接下来寻找color和font-weight 142 | if (parentNodeOfTheFirstTextNode.ParentNode != null) 143 | { 144 | GetGlobalStyleForAllTextBlocks(parentNodeOfTheFirstTextNode.ParentNode, ref shouldBold, ref textBlockGlobalColor); 145 | } 146 | 147 | var styleAttributeValue = parentNodeOfTheFirstTextNode.GetAttributeValue(AttributeStyle, null); 148 | if (styleAttributeValue == null) 149 | { 150 | return; 151 | } 152 | 153 | var attributeValues = styleAttributeValue.Split(";").Select(str => str.Trim()).ToList(); 154 | 155 | // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator 156 | foreach (var styleParam in attributeValues) 157 | { 158 | var paramPair = styleParam.Split(":").Select(str => str.Trim()).ToList(); 159 | switch (paramPair[0]) 160 | { 161 | case AttributeStyleParamColor: 162 | textBlockGlobalColor = paramPair[1].FirstCharToUpper().ToColor(); 163 | break; 164 | case AttributeStyleParamFontWeight: 165 | shouldBold = paramPair[1].Equals("bold", StringComparison.OrdinalIgnoreCase); 166 | break; 167 | } 168 | } 169 | } 170 | 171 | private static void SetEmailHyperLinkInTextBlock(HtmlNode textNode, TextBlock textBlock) 172 | { 173 | if (textNode.SelectSingleNode(XPathANode) == null) 174 | { 175 | return; 176 | } 177 | 178 | var hrefTarget = textNode.SelectSingleNode(XPathANode).GetAttributeValue(AttributeHyperlink, null); 179 | if (hrefTarget == null || !hrefTarget.StartsWith(EmailToScheme)) 180 | { 181 | return; 182 | } 183 | 184 | var targetEmailAddress = hrefTarget.Split(EmailToScheme)[1]; 185 | var textParts = textBlock.Text.Split(targetEmailAddress); 186 | 187 | textBlock.Text = ""; 188 | foreach (var textPart in textParts) 189 | { 190 | if (!string.IsNullOrEmpty(textPart)) 191 | { 192 | textBlock.Inlines.Add(new Run 193 | { 194 | Text = textPart 195 | }); 196 | } 197 | else 198 | { 199 | var hyperlink = new Hyperlink 200 | { 201 | NavigateUri = new Uri(hrefTarget) 202 | }; 203 | 204 | hyperlink.Inlines.Add(new Run 205 | { 206 | Text = targetEmailAddress 207 | }); 208 | 209 | textBlock.Inlines.Add(hyperlink); 210 | } 211 | } 212 | } 213 | 214 | private static async Task GetReferencedThread(string threadId) 215 | { 216 | var refPage = await AnoBbsApiClient.GetReferencedThreadById(threadId); 217 | var htmlDoc = new HtmlDocument(); 218 | htmlDoc.LoadHtml(refPage); 219 | 220 | if (htmlDoc.DocumentNode.ChildNodes.Count > 1 && htmlDoc.DocumentNode.Descendants("br").Any()) 221 | { 222 | foreach (var node in htmlDoc.DocumentNode.SelectNodes(XPathBrNodeAnywhere)) 223 | { 224 | node.Remove(); 225 | } 226 | } 227 | 228 | var refNodeDocument = htmlDoc.DocumentNode.SelectSingleNode(XPathDivWithClassThreadsContent); 229 | var innerText = HtmlEntity.DeEntitize(refNodeDocument.InnerText.Trim()); 230 | 231 | return innerText; 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /XoW/Services/HttpClientService.cs: -------------------------------------------------------------------------------- 1 | using Windows.Web.Http; 2 | using Windows.Web.Http.Filters; 3 | using Windows.Web.Http.Headers; 4 | 5 | namespace XoW.Services 6 | { 7 | internal static class HttpClientService 8 | { 9 | private const string UserAgent = "HavfunClient-UWP"; 10 | 11 | private static readonly object Lock = new object(); 12 | private static HttpClient _httpClient; 13 | private static HttpClient _httpClientForThirdParty; 14 | 15 | public static HttpClient GetHttpClientInstance() 16 | { 17 | if (_httpClient is null) 18 | { 19 | lock (Lock) 20 | { 21 | var httpClient = new HttpClient(new HttpBaseProtocolFilter()); 22 | httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd(UserAgent); 23 | 24 | _httpClient = httpClient; 25 | } 26 | } 27 | 28 | return _httpClient; 29 | } 30 | 31 | public static HttpClient GetHttpClientForThirdPartyInstance() 32 | { 33 | if (_httpClientForThirdParty is null) 34 | { 35 | lock (Lock) 36 | { 37 | _httpClientForThirdParty = new HttpClient(); 38 | } 39 | } 40 | 41 | return _httpClientForThirdParty; 42 | } 43 | 44 | public static void ApplyCookie(string cookieValue) 45 | { 46 | var cookie = new HttpCookiePairHeaderValue(Constants.CookieNameUserHash) 47 | { 48 | Value = cookieValue 49 | }; 50 | 51 | GetHttpClientInstance().DefaultRequestHeaders.Cookie.Clear(); 52 | GetHttpClientInstance().DefaultRequestHeaders.Cookie.Add(cookie); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /XoW/Services/QrCodeService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Windows.Graphics.Imaging; 4 | using Windows.Storage; 5 | using Windows.Storage.Streams; 6 | using Windows.UI.Xaml.Media.Imaging; 7 | using Newtonsoft.Json; 8 | using XoW.Models; 9 | using ZXing; 10 | 11 | namespace XoW.Services 12 | { 13 | public class QrCodeService 14 | { 15 | private static readonly object Lock = new object(); 16 | private static IBarcodeReader _barcodeReader; 17 | 18 | public static IBarcodeReader GetBarcodeReaderInstance() 19 | { 20 | if (_barcodeReader == null) 21 | { 22 | lock (Lock) 23 | { 24 | _barcodeReader = new BarcodeReader 25 | { 26 | AutoRotate = true 27 | }; 28 | } 29 | } 30 | 31 | return _barcodeReader; 32 | } 33 | 34 | public static async Task DecodeBarcodeFromStorageFileAsync(StorageFile file) 35 | { 36 | var barcodeDecoder = GetBarcodeReaderInstance(); 37 | 38 | using var stream = await file.OpenReadAsync(); 39 | var bitmap = new BitmapImage(); 40 | await bitmap.SetSourceAsync(stream); 41 | var bytes = await GetByteArrayFromStream(stream); 42 | var decodeResult = barcodeDecoder.Decode(bytes, bitmap.PixelWidth, bitmap.PixelHeight, RGBLuminanceSource.BitmapFormat.Unknown); 43 | 44 | if (decodeResult == null) 45 | { 46 | throw new AppException(ErrorMessage.QrCodeDecodeFailed); 47 | } 48 | 49 | var content = decodeResult.Text; 50 | var cookie = JsonConvert.DeserializeObject(content); 51 | 52 | return cookie; 53 | } 54 | 55 | private static async Task GetByteArrayFromStream(IRandomAccessStream stream) 56 | { 57 | var bitmapDecoder = await BitmapDecoder.CreateAsync(stream); 58 | var pixelDataProvider = await bitmapDecoder.GetPixelDataAsync(); 59 | return pixelDataProvider.DetachPixelData(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /XoW/Utils/ApplicationConfigurationHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Windows.Storage; 3 | using XoW.Models; 4 | 5 | namespace XoW.Utils 6 | { 7 | public static class ApplicationConfigurationHelper 8 | { 9 | private static readonly ApplicationDataContainer LocalSettings = ApplicationData.Current.LocalSettings; 10 | 11 | public static void SetCurrentCookie(string cookie) 12 | { 13 | LocalSettings.Values[ApplicationSettingsKey.CurrentCookie] = cookie; 14 | GlobalState.ObservableObject.CurrentCookie = cookie; 15 | } 16 | 17 | public static string GetCurrentCookie() => LocalSettings.Values[ApplicationSettingsKey.CurrentCookie]?.ToString(); 18 | 19 | public static void RemoveCurrentCookie() => LocalSettings.Values.Remove(ApplicationSettingsKey.CurrentCookie); 20 | 21 | public static void AddCookie(AnoBbsCookie cookie) 22 | { 23 | if (LocalSettings.Values[ApplicationSettingsKey.AllCookies] is not ApplicationDataCompositeValue cookieListComposite) 24 | { 25 | cookieListComposite = new ApplicationDataCompositeValue(); 26 | } 27 | 28 | if (cookieListComposite.ContainsKey(cookie.Name)) 29 | { 30 | return; 31 | } 32 | 33 | cookieListComposite[cookie.Name] = cookie.Cookie; 34 | 35 | LocalSettings.Values[ApplicationSettingsKey.AllCookies] = cookieListComposite; 36 | } 37 | 38 | public static void DeleteCookie(string cookieName) 39 | { 40 | if (LocalSettings.Values[ApplicationSettingsKey.AllCookies] is not ApplicationDataCompositeValue cookieListComposite) 41 | { 42 | return; 43 | } 44 | 45 | cookieListComposite.Remove(cookieName); 46 | LocalSettings.Values[ApplicationSettingsKey.AllCookies] = cookieListComposite; 47 | } 48 | 49 | public static void LoadAllCookies() 50 | { 51 | if (LocalSettings.Values[ApplicationSettingsKey.AllCookies] is not ApplicationDataCompositeValue cookieListComposite) 52 | { 53 | return; 54 | } 55 | 56 | var parsedCookies = cookieListComposite.Select(cookie => new AnoBbsCookie 57 | { 58 | Name = cookie.Key, 59 | Cookie = cookie.Value.ToString() 60 | }) 61 | .ToList(); 62 | 63 | parsedCookies.ForEach(cookie => GlobalState.Cookies.Add(cookie)); 64 | } 65 | 66 | public static void SetDarkThemeEnabled(bool isDarkThemeEnabled) => LocalSettings.Values[ApplicationSettingsKey.DarkThemeSelected] = isDarkThemeEnabled; 67 | 68 | public static bool IsDarkThemeEnabled() => LocalSettings.Values.ContainsKey(ApplicationSettingsKey.DarkThemeSelected) && (bool)LocalSettings.Values[ApplicationSettingsKey.DarkThemeSelected]; 69 | 70 | public static void SetSubscriptionId(string subscriptionId) => LocalSettings.Values[ApplicationSettingsKey.SubscriptionId] = subscriptionId; 71 | 72 | public static string GetSubscriptionId() => 73 | LocalSettings.Values.ContainsKey(ApplicationSettingsKey.SubscriptionId) 74 | ? LocalSettings.Values[ApplicationSettingsKey.SubscriptionId].ToString() 75 | : null; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /XoW/Utils/CommonUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Windows.Storage; 4 | using Windows.Storage.Pickers; 5 | 6 | namespace XoW.Utils 7 | { 8 | public static class CommonUtils 9 | { 10 | public static async Task OpenFilePickerForSingleImageAsync() 11 | { 12 | var filePicker = new FileOpenPicker(); 13 | filePicker.FileTypeFilter.Add("*"); 14 | 15 | var storageFile = await filePicker.PickSingleFileAsync(); 16 | 17 | if (storageFile == null) 18 | { 19 | return null; 20 | } 21 | 22 | var fileMimeType = storageFile.ContentType; 23 | if (!fileMimeType.StartsWith("image/")) 24 | { 25 | throw new AppException(ErrorMessage.FileIsNotImage); 26 | } 27 | 28 | return storageFile; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /XoW/Utils/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace XoW.Utils 4 | { 5 | public static class StringExtensions 6 | { 7 | public static string FirstCharToUpper(this string str) => 8 | string.IsNullOrWhiteSpace(str) 9 | ? str 10 | : string.Concat(str[0].ToString().ToUpper(), str.Substring(1)); 11 | 12 | public static string ToHexString(this byte[] hexBytes) 13 | { 14 | var stringBuilder = new StringBuilder(); 15 | foreach (var outputByte in hexBytes) 16 | { 17 | stringBuilder.Append(outputByte.ToString("X2")); 18 | } 19 | 20 | return stringBuilder.ToString(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /XoW/Views/ConfirmationContentDialog.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /XoW/Views/ConfirmationContentDialog.xaml.cs: -------------------------------------------------------------------------------- 1 | using Windows.Foundation; 2 | using Windows.UI; 3 | using Windows.UI.Xaml; 4 | using Windows.UI.Xaml.Controls; 5 | using Windows.UI.Xaml.Media; 6 | 7 | namespace XoW.Views 8 | { 9 | public sealed partial class ConfirmationContentDialog : ContentDialog 10 | { 11 | public ConfirmationContentDialog(string title, string content = null, Color? foreGroundColor = null, TypedEventHandler primaryButtonEventHandler = null, TypedEventHandler secondaryButtonEventHandler = null, string primaryButtonContent = null, string secondaryButtonContent = null) 12 | { 13 | InitializeComponent(); 14 | RequestedTheme = ((FrameworkElement)Window.Current.Content).RequestedTheme; 15 | Title = title; 16 | Content = content; 17 | PrimaryButtonText = primaryButtonContent ?? ComponentContent.Confirm; 18 | SecondaryButtonText = secondaryButtonContent ?? ComponentContent.Cancel; 19 | 20 | Foreground = foreGroundColor != null 21 | ? new SolidColorBrush((Color)foreGroundColor) 22 | : new SolidColorBrush(Colors.Black); 23 | 24 | if (primaryButtonEventHandler != null) 25 | { 26 | PrimaryButtonClick += primaryButtonEventHandler; 27 | } 28 | 29 | if (secondaryButtonEventHandler != null) 30 | { 31 | SecondaryButtonClick += secondaryButtonEventHandler; 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /XoW/Views/ContentDialogWithInput.xaml: -------------------------------------------------------------------------------- 1 |  12 | 13 | 14 | 15 | 16 | 18 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /XoW/Views/ContentDialogWithInput.xaml.cs: -------------------------------------------------------------------------------- 1 | using Windows.Foundation; 2 | using Windows.UI.Xaml; 3 | using Windows.UI.Xaml.Controls; 4 | 5 | namespace XoW.Views 6 | { 7 | public sealed partial class ContentDialogWithInput : ContentDialog 8 | { 9 | public ContentDialogWithInput(string title, string primaryButtonText, TypedEventHandler primaryButtonEventHandler = null, TypedEventHandler beforeTextChangingEventHandler = null) 10 | { 11 | InitializeComponent(); 12 | RequestedTheme = ((FrameworkElement)Window.Current.Content).RequestedTheme; 13 | 14 | Title = title; 15 | PrimaryButtonText = primaryButtonText; 16 | 17 | if (primaryButtonEventHandler != null) 18 | { 19 | PrimaryButtonClick += primaryButtonEventHandler; 20 | } 21 | 22 | if (beforeTextChangingEventHandler != null) 23 | { 24 | TextBoxInput.BeforeTextChanging += beforeTextChangingEventHandler; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /XoW/Views/LargeImageViewUserControl.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 23 | 29 | 35 | 36 | 37 | 38 | 43 | 44 | 48 | 49 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /XoW/Views/LargeImageViewUserControl.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Windows.Storage; 4 | using Windows.UI.Xaml; 5 | using Windows.UI.Xaml.Controls; 6 | using Windows.UI.Xaml.Media.Imaging; 7 | 8 | namespace XoW.Views 9 | { 10 | public sealed partial class LargeImageViewUserControl : UserControl 11 | { 12 | public LargeImageViewUserControl() 13 | { 14 | InitializeComponent(); 15 | GlobalState.LargeImageViewObjectReference = this; 16 | 17 | LargeImage.ImageFailed += (_, _) => 18 | { 19 | ProgressRingImageLoad.Visibility = Visibility.Collapsed; 20 | TextBlockImageLoadFailed.Visibility = Visibility.Visible; 21 | }; 22 | } 23 | 24 | private async void OnSaveImageButtonClicked(object sender, RoutedEventArgs args) 25 | { 26 | ButtonSaveImage.IsEnabled = false; 27 | 28 | var imageUri = ((BitmapImage)LargeImage.Source).UriSource; 29 | var imageFile = await StorageFile.CreateStreamedFileFromUriAsync(Path.GetFileName(imageUri.AbsoluteUri), imageUri, null); 30 | var imageFileName = imageFile.Name; 31 | 32 | var targetFolderPath = await KnownFolders.PicturesLibrary.CreateFolderAsync(Constants.ForumName, CreationCollisionOption.OpenIfExists); 33 | await imageFile.CopyAsync(targetFolderPath, imageFileName, NameCollisionOption.ReplaceExisting); 34 | 35 | ButtonSaveImage.IsEnabled = true; 36 | await new NotificationContentDialog(false, $"{ComponentContent.ImageSavedToLocation} {targetFolderPath.Path}").ShowAsync(); 37 | } 38 | 39 | public void ResetState() 40 | { 41 | ProgressRingImageLoad.Visibility = Visibility.Visible; 42 | TextBlockImageLoadFailed.Visibility = Visibility.Collapsed; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /XoW/Views/MainPageHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.Toolkit.Uwp; 6 | using Microsoft.Toolkit.Uwp.UI.Controls; 7 | using Windows.UI.Xaml; 8 | using Windows.UI.Xaml.Controls; 9 | using Windows.UI.Xaml.Media; 10 | using XoW.Models; 11 | using XoW.Services; 12 | using XoW.Utils; 13 | 14 | namespace XoW.Views 15 | { 16 | partial class MainPage : Page 17 | { 18 | private void InitializeStaticResources() 19 | { 20 | // 载入已添加的饼干 21 | ApplicationConfigurationHelper.LoadAllCookies(); 22 | 23 | // 默认以当前选择的饼干发串 24 | NewThreadCookieSelectionComboBox.SelectedItem = GlobalState.Cookies.SingleOrDefault(cookie => cookie.Name == GlobalState.ObservableObject.CurrentCookie); 25 | NewReplyCookieSelectionComboBox.SelectedItem = GlobalState.Cookies.SingleOrDefault(cookie => cookie.Name == GlobalState.ObservableObject.CurrentCookie); 26 | 27 | // 加载订阅ID 28 | GlobalState.ObservableObject.SubscriptionId = ApplicationConfigurationHelper.GetSubscriptionId(); 29 | 30 | var currentCookieName = ApplicationConfigurationHelper.GetCurrentCookie(); 31 | var currentCookieValue = GlobalState.Cookies.SingleOrDefault(cookie => cookie.Name == currentCookieName)?.Cookie; 32 | if (!string.IsNullOrEmpty(currentCookieValue)) 33 | { 34 | HttpClientService.ApplyCookie(currentCookieValue); 35 | } 36 | 37 | CreateEmoticonButtons(NewThreadEmoticonWrapPanel, OnNewThreadEmoticonButtonClicked); 38 | CreateEmoticonButtons(NewReplyEmoticonWrapPanel, OnNewReplyEmoticonButtonClicked); 39 | } 40 | 41 | private static async Task GetCdnUrl() => (await AnoBbsApiClient.GetCdnAsync()).First().Url; 42 | 43 | private async Task RefreshForumsAsync() 44 | { 45 | GlobalState.ForumAndIdLookup = new Dictionary(); 46 | 47 | var forumGroups = await AnoBbsApiClient.GetForumGroupsAsync(); 48 | 49 | // 版面组和版面按照Sort排序,保证以正确的顺序展示 50 | forumGroups = forumGroups.OrderBy(fg => fg.Sort).ToList(); 51 | 52 | forumGroups.ForEach(fg => 53 | { 54 | fg.Forums = fg.Forums.OrderBy(f => f.Sort).ToList(); 55 | }); 56 | 57 | forumGroups.SelectMany(fg => fg.Forums).ToList().ForEach(f => GlobalState.ForumAndIdLookup.Add(f.Name, (f.Id, f.permissionLevel))); 58 | 59 | _navigationItems.Clear(); 60 | 61 | var favouriteThreadsNavigationItem = new NavigationViewItem 62 | { 63 | Content = Constants.FavouriteThreadNavigationItemName, 64 | Name = Constants.FavouriteThreadNavigationItemName, 65 | Icon = new SymbolIcon(Symbol.OutlineStar) 66 | }; 67 | _navigationItems.Add(favouriteThreadsNavigationItem); 68 | 69 | forumGroups.ForEach(fg => 70 | { 71 | // 版面组名作为导航栏Header 72 | var navigationHeader = new NavigationViewItemHeader 73 | { 74 | Content = fg.Name, 75 | Name = fg.Name, 76 | // 在导航栏折叠时,隐藏导航栏Header 77 | Visibility = ForumListNavigation.IsPaneOpen 78 | ? Visibility.Visible 79 | : Visibility.Collapsed 80 | }; 81 | _navigationItems.Add(navigationHeader); 82 | 83 | // 遍历版面组下的版面,依次插入导航栏 84 | fg.Forums.ToList() 85 | .ForEach(f => 86 | { 87 | var navigationItem = new NavigationViewItem 88 | { 89 | Content = f.Name, 90 | Name = f.Name, 91 | // 取版面名第一个字作为图标,在导航栏折叠时展示 92 | Icon = new FontIcon 93 | { 94 | // 是个Windows肯定会带微软雅黑的吧 95 | FontFamily = new FontFamily("Microsoft YaHei"), 96 | Glyph = f.Name.First().ToString() 97 | }, 98 | DataContext = f.Id.ToString() 99 | }; 100 | _navigationItems.Add(navigationItem); 101 | }); 102 | 103 | _navigationItems.Add(new NavigationViewItemSeparator()); 104 | }); 105 | 106 | // 版面导航栏加载完成后,默认选择第一项,即默认展示时间线 107 | ForumListNavigation.SelectedItem = _navigationItems.First(item => item is NavigationViewItem && !_nonForumNavigationItems.Contains(item.Name)); 108 | } 109 | 110 | private async Task RefreshThreads() 111 | { 112 | MainPageProgressBar.Visibility = Visibility.Visible; 113 | 114 | if (GlobalState.CurrentForumId == Constants.TimelineForumId) 115 | { 116 | var incrementalLoadingCollection = new IncrementalLoadingCollection(); 117 | await incrementalLoadingCollection.RefreshAsync(); 118 | ThreadsListView.ItemsSource = incrementalLoadingCollection; 119 | } 120 | else 121 | { 122 | var incrementalLoadingCollection = new IncrementalLoadingCollection(); 123 | await incrementalLoadingCollection.RefreshAsync(); 124 | ThreadsListView.ItemsSource = incrementalLoadingCollection; 125 | } 126 | 127 | GlobalState.ObservableObject.ForumName = GlobalState.ForumAndIdLookup.Single(lookup => lookup.Value.forumId == GlobalState.CurrentForumId).Key; 128 | 129 | MainPageProgressBar.Visibility = Visibility.Collapsed; 130 | } 131 | 132 | private async Task RefreshRepliesForXamlBinding() 133 | { 134 | await RefreshReplies(); 135 | } 136 | 137 | private async Task RefreshReplies(int pageNumber = 1) 138 | { 139 | MainPageProgressBar.Visibility = Visibility.Visible; 140 | 141 | var threadReplySource = new ThreadReplySource(pageNumber); 142 | var incrementalLoadingCollection = new IncrementalLoadingCollection(threadReplySource); 143 | await incrementalLoadingCollection.RefreshAsync(); 144 | RepliesListView.ItemsSource = incrementalLoadingCollection; 145 | 146 | MainPageProgressBar.Visibility = Visibility.Collapsed; 147 | } 148 | 149 | private async Task RefreshPoOnlyReplies(int pageNumber = 1) 150 | { 151 | MainPageProgressBar.Visibility = Visibility.Visible; 152 | 153 | var poOnlyThreadReplySource = new PoOnlyThreadReplySource(pageNumber); 154 | var incrementalLoadingCollection = new IncrementalLoadingCollection(poOnlyThreadReplySource); 155 | await incrementalLoadingCollection.RefreshAsync(); 156 | RepliesListView.ItemsSource = incrementalLoadingCollection; 157 | 158 | MainPageProgressBar.Visibility = Visibility.Collapsed; 159 | } 160 | 161 | private async Task RefreshSubscriptions() 162 | { 163 | MainPageProgressBar.Visibility = Visibility.Visible; 164 | 165 | var itemsSource = new IncrementalLoadingCollection(); 166 | itemsSource.OnEndLoading = () => 167 | { 168 | foreach (var item in itemsSource) 169 | { 170 | var contentParentStackPanel = item.Children.Single(element => ((StackPanel)element).Name == ComponentsBuilder.TopLevelStackPanel) as StackPanel; 171 | var headerGrid = contentParentStackPanel.Children.Single(element => ((Grid)element).Name == ComponentsBuilder.ThreadHeaderParentGrid) as Grid; 172 | var stackPanelForDeleteButton = headerGrid.Children.Single(element => ((StackPanel)element).Name == ComponentsBuilder.StackPanelForDeleteButton) as StackPanel; 173 | var buttonForDeleteSubscription = stackPanelForDeleteButton.Children.Where(element => element is Button).Single(button => ((Button)button).Name == ComponentsBuilder.ButtonDeleteSubscriptionName) as Button; 174 | 175 | // 确保这个EventHandler只被注册一次 176 | buttonForDeleteSubscription.Click -= OnDeleteSubscriptionButtonClicked; 177 | buttonForDeleteSubscription.Click += OnDeleteSubscriptionButtonClicked; 178 | } 179 | }; 180 | 181 | await itemsSource.RefreshAsync(); 182 | ThreadsListView.ItemsSource = itemsSource; 183 | 184 | MainPageProgressBar.Visibility = Visibility.Collapsed; 185 | } 186 | 187 | private void ResetAndShowRepliesPanel() 188 | { 189 | ButtonPoOnly.IsChecked = GlobalState.isPoOnly; 190 | ContentRepliesGrid.Visibility = Visibility.Visible; 191 | } 192 | 193 | private void ShowSettingsGrid() 194 | { 195 | ContentGrid.Visibility = Visibility.Collapsed; 196 | SettingsPage.Visibility = Visibility.Visible; 197 | } 198 | 199 | private void ShowContentGrid() 200 | { 201 | ContentGrid.Visibility = Visibility.Visible; 202 | SettingsPage.Visibility = Visibility.Collapsed; 203 | } 204 | 205 | private void ShowNewThreadPanel() => NewThreadPanelGrid.Visibility = Visibility.Visible; 206 | 207 | private async Task HideNewThreadPanel(bool isNewThreadSent = false) 208 | { 209 | if (!isNewThreadSent && NewThreadPanelGrid.Visibility == Visibility.Visible && !string.IsNullOrWhiteSpace(TextBoxNewThreadContent.Text)) 210 | { 211 | await new ConfirmationContentDialog(ComponentContent.Notification, ConfirmationMessage.KeepContentConfirmation, secondaryButtonEventHandler: (_, _) => ResetNewThreadPanel(), primaryButtonContent: ComponentContent.KeepingIt, secondaryButtonContent: ComponentContent.DiscardIt).ShowAsync(); 212 | } 213 | 214 | NewThreadPanelGrid.Visibility = Visibility.Collapsed; 215 | } 216 | 217 | private void ShowNewReplyPanel() => NewReplyPanelGrid.Visibility = Visibility.Visible; 218 | 219 | private async Task HideNewReplyPanel(bool isNewReplySent = false) 220 | { 221 | if (!isNewReplySent && NewReplyPanelGrid.Visibility == Visibility.Visible && !string.IsNullOrWhiteSpace(TextBoxNewReplyContent.Text)) 222 | { 223 | await new ConfirmationContentDialog(ComponentContent.Notification, ConfirmationMessage.KeepContentConfirmation, secondaryButtonEventHandler: (_, _) => ResetNewReplyPanel(), primaryButtonContent: ComponentContent.KeepingIt, secondaryButtonContent: ComponentContent.DiscardIt).ShowAsync(); 224 | } 225 | 226 | NewReplyPanelGrid.Visibility = Visibility.Collapsed; 227 | } 228 | 229 | private void HideLargeImageView() 230 | { 231 | LargeImageView.Visibility = Visibility.Collapsed; 232 | GlobalState.LargeImageViewObjectReference.ResetState(); 233 | } 234 | 235 | private void ResetNewThreadPanel() 236 | { 237 | TextBoxNewThreadUserName.Text = ""; 238 | TextBoxNewThreadEmail.Text = ""; 239 | TextBoxNewThreadTitle.Text = ""; 240 | TextBoxNewThreadContent.Text = ""; 241 | NewThreadCookieSelectionComboBox.SelectedItem = GlobalState.Cookies.SingleOrDefault(cookie => cookie.Name == GlobalState.ObservableObject.CurrentCookie); 242 | ForumSelectionComboBox.SelectedIndex = 0; 243 | ButtonNewThreadAttachPicture.DataContext = null; 244 | CheckBoxNewThreadWaterMark.IsChecked = true; 245 | ImagePreviewStackPanel.Visibility = Visibility.Collapsed; 246 | } 247 | 248 | private void ResetNewReplyPanel() 249 | { 250 | TextBoxNewReplyUserName.Text = ""; 251 | TextBoxNewReplyEmail.Text = ""; 252 | TextBoxNewReplyTitle.Text = ""; 253 | TextBoxNewReplyContent.Text = ""; 254 | NewReplyCookieSelectionComboBox.SelectedItem = GlobalState.Cookies.SingleOrDefault(cookie => cookie.Name == GlobalState.ObservableObject.CurrentCookie); 255 | ButtonNewReplyAttachPicture.DataContext = null; 256 | CheckBoxNewReplyWaterMark.IsChecked = true; 257 | ReplyImagePreviewStackPanel.Visibility = Visibility.Collapsed; 258 | } 259 | 260 | private void DisableSendButtonAndShowProgressBar(Button sendButton) 261 | { 262 | sendButton.IsEnabled = false; 263 | MainPageProgressBar.Visibility = Visibility.Visible; 264 | } 265 | 266 | private void EnableSendButtonAndHideProgressBar(Button sendButton) 267 | { 268 | sendButton.IsEnabled = true; 269 | MainPageProgressBar.Visibility = Visibility.Collapsed; 270 | } 271 | 272 | private void CreateEmoticonButtons(WrapPanel parentWrapPanel, RoutedEventHandler clickEventHandler) 273 | { 274 | foreach (var emoticon in Constants.Emoticons) 275 | { 276 | var button = new Button 277 | { 278 | Content = emoticon.Key, 279 | DataContext = emoticon.Value 280 | }; 281 | 282 | button.Click += clickEventHandler; 283 | 284 | parentWrapPanel.Children.Add(button); 285 | } 286 | } 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /XoW/Views/NotificationContentDialog.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /XoW/Views/NotificationContentDialog.xaml.cs: -------------------------------------------------------------------------------- 1 | using Windows.UI.Xaml; 2 | using Windows.UI.Xaml.Controls; 3 | 4 | // The Content Dialog item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 5 | 6 | namespace XoW.Views 7 | { 8 | public sealed partial class NotificationContentDialog : ContentDialog 9 | { 10 | public NotificationContentDialog(bool isErrorPopup, string content) 11 | { 12 | InitializeComponent(); 13 | RequestedTheme = ((FrameworkElement)Window.Current.Content).RequestedTheme; 14 | Title = isErrorPopup 15 | ? ComponentContent.Error 16 | : ComponentContent.Notification; 17 | Content = content; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /XoW/Views/ReportThreadContentDialog.xaml: -------------------------------------------------------------------------------- 1 |  14 | 15 | 16 | 17 | 18 | 20 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /XoW/Views/ReportThreadContentDialog.xaml.cs: -------------------------------------------------------------------------------- 1 | using Windows.UI.Xaml; 2 | using Windows.UI.Xaml.Controls; 3 | using XoW.Services; 4 | 5 | // The Content Dialog item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 6 | 7 | namespace XoW.Views 8 | { 9 | public sealed partial class ReportThreadContentDialog : ContentDialog 10 | { 11 | private readonly string _threadId; 12 | 13 | public ReportThreadContentDialog(string threadId) 14 | { 15 | InitializeComponent(); 16 | RequestedTheme = ((FrameworkElement)Window.Current.Content).RequestedTheme; 17 | _threadId = threadId; 18 | } 19 | 20 | private async void ContentDialogPrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) 21 | { 22 | var threadId = _threadId; 23 | var reportReason = TextBoxReportReason.Text; 24 | 25 | var newReportThreadContent = $">>{threadId}\n{reportReason}"; 26 | 27 | await AnoBbsApiClient.PostNewReport(newReportThreadContent); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /XoW/Views/SettingsPage.xaml: -------------------------------------------------------------------------------- 1 |  11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 33 | 37 | 40 | 42 | 43 | 47 | 48 | 49 | 52 | 53 | 62 | 63 | 64 | 117 | 118 | 119 | 120 | 121 | 122 | 126 | 127 | 128 | 129 | 130 | 131 | 133 | 134 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 147 | 154 | 155 | 159 | 160 | 161 | 162 | 163 | 165 | 166 | 167 | 168 | 169 | 171 | 172 | 173 | 174 | 175 | 179 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /XoW/Views/SettingsPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Windows.UI; 4 | using Windows.UI.Xaml; 5 | using Windows.UI.Xaml.Controls; 6 | using Windows.UI.Xaml.Media; 7 | using XoW.Models; 8 | using XoW.Services; 9 | using XoW.Utils; 10 | 11 | namespace XoW.Views 12 | { 13 | public sealed partial class SettingsPage : UserControl 14 | { 15 | public SettingsPage() 16 | { 17 | InitializeComponent(); 18 | } 19 | 20 | private void OnCookieClicked(object sender, ItemClickEventArgs args) 21 | { 22 | var selectedCookie = args.ClickedItem as AnoBbsCookie; 23 | var cookieName = selectedCookie.Name; 24 | var cookieValue = selectedCookie.Cookie; 25 | 26 | ApplicationConfigurationHelper.SetCurrentCookie(cookieName); 27 | HttpClientService.ApplyCookie(cookieValue); 28 | } 29 | 30 | private void OnSubscriptionIdTextBoxTextChanged(object sender, TextChangedEventArgs e) 31 | { 32 | var newSubscriptionId = TextBoxSubscriptionId.Text; 33 | UpdateSubscriptionId(newSubscriptionId); 34 | } 35 | 36 | private async void OnLoadImageButtonClicked(object sender, RoutedEventArgs args) 37 | { 38 | var storageFile = await CommonUtils.OpenFilePickerForSingleImageAsync(); 39 | 40 | var cookie = await QrCodeService.DecodeBarcodeFromStorageFileAsync(storageFile); 41 | 42 | if (!GlobalState.Cookies.Contains(cookie)) 43 | { 44 | GlobalState.Cookies.Add(cookie); 45 | ApplicationConfigurationHelper.AddCookie(cookie); 46 | } 47 | } 48 | 49 | private async void OnDeleteCookieButtonClicked(object sender, RoutedEventArgs args) => 50 | await new ConfirmationContentDialog(ConfirmationMessage.DeleteCookieConfirmation, 51 | primaryButtonEventHandler: (_, _) => 52 | { 53 | var cookieName = ((Button)sender).DataContext?.ToString(); 54 | DeleteCookie(cookieName); 55 | }).ShowAsync(); 56 | 57 | /// 58 | /// 保存是否开启夜间模式的配置,并设定应用全局主题 59 | /// 60 | private void OnNightModeSwitchToggled(object sender, RoutedEventArgs args) 61 | { 62 | var isDarkModeEnabled = ((ToggleSwitch)sender).IsOn; 63 | ApplicationConfigurationHelper.SetDarkThemeEnabled(isDarkModeEnabled); 64 | 65 | #region 设定应用全局主题 66 | var frameworkElementRoot = Window.Current.Content as FrameworkElement; 67 | frameworkElementRoot.RequestedTheme = isDarkModeEnabled 68 | ? ElementTheme.Dark 69 | : ElementTheme.Light; 70 | #endregion 71 | 72 | #region 设定部分手动指定颜色的控件的新颜色 73 | var borderAndBackgroundColor = isDarkModeEnabled 74 | ? new SolidColorBrush(Colors.Black) 75 | : new SolidColorBrush(Colors.LightGray); 76 | GlobalState.ObservableObject.BackgroundAndBorderColorBrush = borderAndBackgroundColor; 77 | 78 | var listViewBackgroundColor = isDarkModeEnabled 79 | ? new SolidColorBrush(Colors.Black) 80 | : new SolidColorBrush(Colors.White); 81 | GlobalState.ObservableObject.ListViewBackgroundColorBrush = listViewBackgroundColor; 82 | #endregion 83 | } 84 | 85 | /// 86 | /// 应用加载时会触发此事件,此时载入是否开启夜间模式的设定 87 | /// 88 | private void OnNightModeSwitchLoaded(object sender, RoutedEventArgs args) 89 | { 90 | var isDarkModeEnabled = ApplicationConfigurationHelper.IsDarkThemeEnabled(); 91 | 92 | // 该操作会触发 ToggleSwitch 的 Toggled 事件 93 | ((ToggleSwitch)sender).IsOn = isDarkModeEnabled; 94 | } 95 | 96 | private async void OnGenerateSubscriptionButtonClicked(object sender, RoutedEventArgs args) 97 | { 98 | if (!string.IsNullOrEmpty(GlobalState.ObservableObject.SubscriptionId)) 99 | { 100 | await new ConfirmationContentDialog(ConfirmationMessage.GenerateNewSubscriptionIdConfirmationTitle, ConfirmationMessage.GenerateNewSubscriptionIdConfirmationContent, Colors.Red, (_, _) => GenerateNewSubscriptionId()).ShowAsync(); 101 | 102 | return; 103 | } 104 | 105 | GenerateNewSubscriptionId(); 106 | } 107 | 108 | private static void DeleteCookie(string cookieName) 109 | { 110 | if (cookieName == GlobalState.ObservableObject.CurrentCookie) 111 | { 112 | ApplicationConfigurationHelper.RemoveCurrentCookie(); 113 | GlobalState.ObservableObject.CurrentCookie = null; 114 | } 115 | 116 | ApplicationConfigurationHelper.DeleteCookie(cookieName); 117 | GlobalState.Cookies.Remove(GlobalState.Cookies.Single(cookie => cookie.Name == cookieName)); 118 | } 119 | 120 | private static void GenerateNewSubscriptionId() 121 | { 122 | var newSubscriptionId = Guid.NewGuid().ToString(); 123 | UpdateSubscriptionId(newSubscriptionId); 124 | } 125 | 126 | private static void UpdateSubscriptionId(string newSubscriptionId) 127 | { 128 | GlobalState.ObservableObject.SubscriptionId = newSubscriptionId; 129 | ApplicationConfigurationHelper.SetSubscriptionId(newSubscriptionId); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /XoW/XoW.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | x86 7 | {4D0FA67F-B8CC-45DB-9D72-C47735FC9380} 8 | AppContainerExe 9 | Properties 10 | XoW 11 | XoW 12 | zh-cn 13 | UAP 14 | 10.0.19041.0 15 | 10.0.17763.0 16 | 14 17 | 512 18 | {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 19 | true 20 | True 21 | default 22 | True 23 | False 24 | False 25 | True 26 | Always 27 | x86|x64|arm|arm64 28 | 0 29 | AD67E5FF61B58ACB1DCC78C81C028DFA9B46D728 30 | SHA256 31 | 32 | 33 | 34 | 35 | true 36 | bin\x86\Debug\ 37 | TRACE;DEBUG;CODE_ANALYSIS 38 | ;2008 39 | full 40 | x86 41 | false 42 | prompt 43 | true 44 | 45 | 46 | bin\x86\Release\ 47 | NETFX_CORE;WINDOWS_UWP 48 | true 49 | ;2008 50 | pdbonly 51 | x86 52 | false 53 | prompt 54 | true 55 | true 56 | 57 | 58 | true 59 | bin\ARM\Debug\ 60 | TRACE;DEBUG;CODE_ANALYSIS 61 | ;2008 62 | full 63 | ARM 64 | false 65 | prompt 66 | true 67 | 68 | 69 | bin\ARM\Release\ 70 | NETFX_CORE;WINDOWS_UWP 71 | true 72 | ;2008 73 | pdbonly 74 | ARM 75 | false 76 | prompt 77 | true 78 | true 79 | 80 | 81 | true 82 | bin\ARM64\Debug\ 83 | TRACE;DEBUG;CODE_ANALYSIS 84 | ;2008 85 | full 86 | ARM64 87 | false 88 | prompt 89 | true 90 | true 91 | 92 | 93 | bin\ARM64\Release\ 94 | NETFX_CORE;WINDOWS_UWP 95 | true 96 | ;2008 97 | pdbonly 98 | ARM64 99 | false 100 | prompt 101 | true 102 | true 103 | 104 | 105 | true 106 | bin\x64\Debug\ 107 | TRACE;DEBUG;CODE_ANALYSIS 108 | ;2008 109 | full 110 | x64 111 | false 112 | prompt 113 | true 114 | MinimumRecommendedRules.ruleset 115 | true 116 | 117 | 118 | bin\x64\Release\ 119 | NETFX_CORE;WINDOWS_UWP 120 | true 121 | ;2008 122 | pdbonly 123 | x64 124 | false 125 | prompt 126 | true 127 | true 128 | 129 | 130 | PackageReference 131 | 132 | 133 | 134 | App.xaml 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | ConfirmationContentDialog.xaml 148 | 149 | 150 | ContentDialogWithInput.xaml 151 | 152 | 153 | LargeImageViewUserControl.xaml 154 | 155 | 156 | MainPage.xaml 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | NotificationContentDialog.xaml 176 | 177 | 178 | ReportThreadContentDialog.xaml 179 | 180 | 181 | SettingsPage.xaml 182 | 183 | 184 | 185 | 186 | Designer 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | MSBuild:Compile 249 | Designer 250 | 251 | 252 | Designer 253 | MSBuild:Compile 254 | 255 | 256 | Designer 257 | MSBuild:Compile 258 | 259 | 260 | Designer 261 | MSBuild:Compile 262 | 263 | 264 | MSBuild:Compile 265 | Designer 266 | 267 | 268 | Designer 269 | MSBuild:Compile 270 | 271 | 272 | Designer 273 | MSBuild:Compile 274 | 275 | 276 | Designer 277 | MSBuild:Compile 278 | 279 | 280 | 281 | 282 | 1.11.54 283 | 284 | 285 | 2.2.8 286 | 287 | 288 | 6.2.14 289 | 290 | 291 | 7.1.3 292 | 293 | 294 | 7.1.3 295 | 296 | 297 | 2.8.5 298 | 299 | 300 | 13.0.3 301 | 302 | 303 | 7.0.0 304 | 305 | 306 | 7.0.0 307 | 308 | 309 | 0.16.9 310 | 311 | 312 | 313 | 314 | 14.0 315 | 316 | 317 | CODE_ANALYSIS 318 | 319 | 320 | CODE_ANALYSIS 321 | 322 | 323 | 330 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | 4 | pool: 5 | vmImage: 'windows-latest' 6 | 7 | variables: 8 | solution: '**/*.sln' 9 | buildPlatform: 'x86|x64|ARM|ARM64' 10 | buildConfiguration: 'Release' 11 | appxPackageDir: '$(build.artifactStagingDirectory)\AppxPackages\\' 12 | 13 | steps: 14 | - task: NuGetToolInstaller@1 15 | 16 | - task: NuGetCommand@2 17 | inputs: 18 | restoreSolution: '$(solution)' 19 | 20 | - task: DownloadSecureFile@1 21 | name: AppConfig 22 | displayName: Download AppConfig.xml from Secure File 23 | inputs: 24 | secureFile: AppConfig.xml 25 | 26 | - task: CopyFiles@2 27 | inputs: 28 | SourceFolder: '$(Agent.TempDirectory)' 29 | Contents: AppConfig.xml 30 | TargetFolder: '$(Build.SourcesDirectory)\XoW\' 31 | 32 | - task: VSBuild@1 33 | inputs: 34 | solution: '$(solution)' 35 | configuration: '$(buildConfiguration)' 36 | msbuildArgs: >- 37 | /p:AppxBundlePlatforms="$(buildPlatform)" 38 | /p:AppxPackageDir="$(appxPackageDir)" 39 | /p:AppxBundle=Always 40 | /p:UapAppxPackageBuildMode=StoreUpload 41 | 42 | - task: CopyFiles@2 43 | displayName: 'Copy Files to: $(build.artifactstagingdirectory)' 44 | inputs: 45 | SourceFolder: '$(system.defaultworkingdirectory)' 46 | Contents: '**\bin\$(BuildConfiguration)\**' 47 | TargetFolder: '$(build.artifactstagingdirectory)' 48 | 49 | - task: PublishBuildArtifacts@1 50 | displayName: 'Publish Artifact: drop' 51 | inputs: 52 | pathToPublish: '$(build.artifactstagingdirectory)' 53 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | --------------------------------------------------------------------------------