├── .dockerignore ├── .gitattributes ├── .gitignore ├── AlbumViewer.png ├── AlbumViewerMobile.png ├── AlbumViewerNetCore.sln ├── AlbumViewerSqlServerData.sql ├── AlbumViewer_Docker.websurge ├── AlbumViewer_WebSurge_HttpRequests.websurge ├── CONTRIBUTING.md ├── LICENSE ├── Readme.md ├── docker-compose.yml ├── docker └── app │ └── Dockerfile └── src ├── AlbumViewerAngular ├── .angular-cli - Copy.json ├── .angular-cli.json ├── .editorconfig ├── .gitignore ├── README.md ├── package.json ├── src │ ├── app │ │ ├── albums │ │ │ ├── albumDisplay.html │ │ │ ├── albumDisplay.ts │ │ │ ├── albumEditor.html │ │ │ ├── albumEditor.ts │ │ │ ├── albumList.html │ │ │ ├── albumList.ts │ │ │ ├── albumService.ts │ │ │ ├── albumSongList.html │ │ │ └── albumSongList.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── artists │ │ │ ├── artistDisplay.html │ │ │ ├── artistDisplay.ts │ │ │ ├── artistEditor.html │ │ │ ├── artistEditor.ts │ │ │ ├── artistList.html │ │ │ ├── artistList.ts │ │ │ └── artistService.ts │ │ ├── business │ │ │ ├── HttpClient.ts │ │ │ ├── appConfiguration.ts │ │ │ ├── entities.ts │ │ │ └── userInfo.ts │ │ ├── common │ │ │ ├── animations.ts │ │ │ ├── appFooter.html │ │ │ ├── appFooter.ts │ │ │ ├── appHeader.html │ │ │ ├── appHeader.ts │ │ │ ├── errorDisplay.ts │ │ │ ├── login.html │ │ │ └── login.ts │ │ └── options │ │ │ ├── about.html │ │ │ ├── about.ts │ │ │ ├── options.html │ │ │ └── options.ts │ ├── css │ │ └── albumviewer.css │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── images │ │ ├── RockLogo.png │ │ ├── artists.png │ │ ├── artists32.png │ │ ├── gear.png │ │ ├── headphone-head.png │ │ ├── icon.png │ │ ├── record.png │ │ ├── search.png │ │ ├── search_box.png │ │ ├── search_box2.png │ │ ├── webconnectionicon.png │ │ ├── webconnectionlogo.png │ │ └── westwindtext.png │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── scripts │ │ └── web-animations.min.js │ ├── touch-icon.png │ ├── touch-icon32.png │ ├── tsconfig.app.json │ └── tsconfig.spec.json ├── tsconfig.json └── tslint.json ├── AlbumViewerAngularSimple ├── .angular-cli.json ├── .editorconfig ├── .gitignore ├── README.md ├── package.json ├── src │ ├── app │ │ ├── albums │ │ │ ├── albumEditor.html │ │ │ ├── albumEditor.ts │ │ │ ├── albumList.html │ │ │ └── albumList.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ └── business │ │ │ └── entities.ts │ ├── assets │ │ └── .gitkeep │ ├── css │ │ ├── albumviewer.css │ │ └── albumviewer.min.css │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── images │ │ ├── RockLogo.png │ │ ├── artists.png │ │ ├── artists32.png │ │ ├── gear.png │ │ ├── headphone-head.png │ │ ├── icon.png │ │ ├── record.png │ │ ├── search.png │ │ ├── search_box.png │ │ ├── search_box2.png │ │ ├── webconnectionicon.png │ │ ├── webconnectionlogo.png │ │ └── westwindtext.png │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ ├── touch-icon.png │ ├── touch-icon32.png │ ├── tsconfig.app.json │ └── tsconfig.spec.json ├── tsconfig.json └── tslint.json ├── AlbumViewerBusiness ├── AccountRepository.cs ├── AlbumRepository.cs ├── AlbumViewerBusiness.csproj ├── ArtistRepository.cs ├── Models │ ├── AlbumViewerContext.cs │ ├── AlbumViewerDataImporter.cs │ └── AlbumViewerEntities.cs ├── Properties │ └── AssemblyInfo.cs └── Westwind.BusinessObjects │ ├── EntityFrameworkRepository.cs │ └── Utilities │ ├── DataAccessBase.cs │ ├── DataUtils.cs │ ├── DbSetExtensions.cs │ └── ReflectionUtils.cs ├── AlbumViewerNetCore ├── .editorconfig ├── AlbumViewer.png ├── AlbumViewerNetCore.csproj ├── AlbumViewer_WebSurge_HttpRequests.websurge ├── Controllers │ ├── AccountController.cs │ ├── AlbumViewerApiController.cs │ ├── AlbumViewerMvcController.NOTUSED │ ├── HelloWorldController.cs │ ├── HomeController.cs │ └── TagHelpers │ │ └── ErrorDisplayTagHelper.cs ├── Program.cs ├── Project_Readme.html ├── Properties │ └── launchSettings.json ├── Startup.cs ├── Views │ ├── AlbumViewerMvc │ │ ├── Album.cshtml │ │ ├── Albums.cshtml │ │ ├── Artists.cshtml │ │ └── TagHelpers.cshtml │ ├── Home │ │ ├── About.cshtml │ │ ├── Contact.cshtml │ │ ├── Error.cshtml │ │ └── Index.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ └── _Layout.cshtml │ ├── _GlobalImport.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── _Code │ └── ErrorHandling │ │ ├── ApiError.cs │ │ ├── ApiException.cs │ │ └── ApiExceptionFilter.cs ├── albums.js ├── appsettings.json ├── bundleconfig.json ├── runtimeconfig.template.json ├── web.config └── wwwroot │ ├── 3rdpartylicenses.txt │ ├── favicon.ico │ ├── fontawesome-webfont.674f50d287a8c48dc19b.eot │ ├── fontawesome-webfont.912ec66d7572ff821749.svg │ ├── fontawesome-webfont.af7ae505a9eed503f8b8.woff2 │ ├── fontawesome-webfont.b06871f281fee6b241d6.ttf │ ├── fontawesome-webfont.fee66e712a8a08eef580.woff │ ├── glyphicons-halflings-regular.448c34a56d699c29117a.woff2 │ ├── glyphicons-halflings-regular.89889688147bd7575d63.svg │ ├── glyphicons-halflings-regular.e18bbf611f2a2e43afc0.ttf │ ├── glyphicons-halflings-regular.f4769f9bdb7466be6508.eot │ ├── glyphicons-halflings-regular.fa2772327f55d8198301.woff │ ├── images │ ├── RockLogo.png │ ├── artists.png │ ├── artists32.png │ ├── gear.png │ ├── headphone-head.png │ ├── icon.png │ ├── record.png │ ├── search.png │ ├── search_box.png │ ├── search_box2.png │ ├── webconnectionicon.png │ ├── webconnectionlogo.png │ └── westwindtext.png │ ├── index.html │ ├── inline.6ab1992b1d6b43743130.bundle.js │ ├── main.d5f34125c7ed7eefbff9.bundle.js │ ├── polyfills.bebf6cc3ec946aa280a0.bundle.js │ ├── scripts.aabcab8cf45f67dea95f.bundle.js │ ├── styles.15901b41a374afce732a.bundle.css │ ├── touch-icon.png │ └── vendor.5013a47f17b192bbdbea.bundle.js └── Westwind.Utilities ├── Data ├── ValidationErrorCollection.cs └── ValidationErrors.cs ├── Properties └── AssemblyInfo.cs ├── Utilities ├── DataUtils.cs └── StringUtils.cs └── Westwind.Utilities.csproj /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | 3 | **/Obj/ 4 | **/obj/ 5 | **/bin/ 6 | **/Bin/ 7 | .vs/ 8 | *.xap 9 | *.user 10 | /TestResults 11 | *.vspscc 12 | *.vssscc 13 | *.suo 14 | *.cache 15 | *.docstates 16 | _ReSharper.* 17 | *.csproj.user 18 | *[Rr]e[Ss]harper.user 19 | _ReSharper.*/ 20 | packages/* 21 | artifacts/* 22 | msbuild.log 23 | PublishProfiles/ 24 | *.psess 25 | *.vsp 26 | *.pidb 27 | *.userprefs 28 | *DS_Store 29 | *.ncrunchsolution 30 | *.log 31 | *.vspx 32 | /.symbols 33 | nuget.exe 34 | *net45.csproj 35 | *k10.csproj 36 | App_Data/ 37 | bower_components 38 | node_modules 39 | *.sln.ide 40 | *.ng.ts 41 | *.sln.ide 42 | .build/ 43 | .testpublish/ 44 | launchSettings.json 45 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.doc diff=astextplain 2 | *.DOC diff=astextplain 3 | *.docx diff=astextplain 4 | *.DOCX diff=astextplain 5 | *.dot diff=astextplain 6 | *.DOT diff=astextplain 7 | *.pdf diff=astextplain 8 | *.PDF diff=astextplain 9 | *.rtf diff=astextplain 10 | *.RTF diff=astextplain 11 | 12 | *.jpg binary 13 | *.png binary 14 | *.gif binary 15 | 16 | *.cs text=auto diff=csharp 17 | *.vb text=auto 18 | *.resx text=auto 19 | *.c text=auto 20 | *.cpp text=auto 21 | *.cxx text=auto 22 | *.h text=auto 23 | *.hxx text=auto 24 | *.py text=auto 25 | *.rb text=auto 26 | *.java text=auto 27 | *.html text=auto 28 | *.htm text=auto 29 | *.css text=auto 30 | *.scss text=auto 31 | *.sass text=auto 32 | *.less text=auto 33 | *.js text=auto 34 | *.lisp text=auto 35 | *.clj text=auto 36 | *.sql text=auto 37 | *.php text=auto 38 | *.lua text=auto 39 | *.m text=auto 40 | *.asm text=auto 41 | *.erl text=auto 42 | *.fs text=auto 43 | *.fsx text=auto 44 | *.hs text=auto 45 | 46 | *.csproj text=auto 47 | *.vbproj text=auto 48 | *.fsproj text=auto 49 | *.dbproj text=auto 50 | *.sln text=auto eol=crlf 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /tmp 4 | /nginx/wwwroot 5 | 6 | # dependencies 7 | node_modules/ 8 | bower_components/ 9 | 10 | # IDEs and editors 11 | /.idea 12 | /.vs 13 | .vscode 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # Dotnet 22 | [Oo]bj/ 23 | [Bb]in/ 24 | *.xap 25 | *.user 26 | /TestResults 27 | *.vspscc 28 | *.vssscc 29 | *.suo 30 | *.cache 31 | *.docstates 32 | *.log 33 | *.log.* 34 | _ReSharper.* 35 | *.csproj.user 36 | *[Rr]e[Ss]harper.user 37 | _ReSharper.*/ 38 | packages/* 39 | artifacts/* 40 | msbuild.log 41 | PublishProfiles/ 42 | *.psess 43 | *.vsp 44 | *.pidb 45 | *.userprefs 46 | *DS_Store 47 | *.ncrunchsolution 48 | *.log 49 | *.vspx 50 | /.symbols 51 | nuget.exe 52 | build/ 53 | *net45.csproj 54 | *k10.csproj 55 | App_Data/ 56 | *.sln.ide 57 | */*.Spa/public/* 58 | */*.Spa/wwwroot/* 59 | *.ng.ts 60 | *.sln.ide 61 | __*.htm 62 | 63 | # Application 64 | AlbumViewer/ 65 | src/AlbumViewerVNext/wwwroot/web.config__X 66 | thumbs.db 67 | .vs 68 | .vs/config/applicationhost.config 69 | publishprofile.xml 70 | .idea 71 | dist 72 | /AlbumViewerVnext.azurewebsites.net.PublishSettings 73 | src/AlbumViewerAspNet5/project.lock.json 74 | AlbumViewer.publishsettings 75 | *.sqlite 76 | *.lock.json 77 | src/AlbumViewerBusiness/project.lock.json 78 | src/AlbumViewerNetCore/project.lock.json 79 | src/Westwind.Utilitities/project.lock.json 80 | -------------------------------------------------------------------------------- /AlbumViewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/AlbumViewer.png -------------------------------------------------------------------------------- /AlbumViewerMobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/AlbumViewerMobile.png -------------------------------------------------------------------------------- /AlbumViewerNetCore.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0A329D36-19A2-4F54-B3C7-17640DDBF44C}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B10A5E44-A157-44F6-95C6-7AF5626B5D4C}" 9 | ProjectSection(SolutionItems) = preProject 10 | AlbumViewer_WebSurge_HttpRequests.websurge = AlbumViewer_WebSurge_HttpRequests.websurge 11 | Readme.md = Readme.md 12 | EndProjectSection 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AlbumViewerNetCore", "src\AlbumViewerNetCore\AlbumViewerNetCore.csproj", "{00D86129-ADE5-403F-96AE-9263C01BE2E4}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AlbumViewerBusiness", "src\AlbumViewerBusiness\AlbumViewerBusiness.csproj", "{209EAF20-38E2-4C67-9557-9247A07213E6}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Westwind.Utilities", "src\Westwind.Utilities\Westwind.Utilities.csproj", "{4D936F4A-C6BD-4724-810F-949AB4D0BD36}" 19 | EndProject 20 | Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "AlbumViewerAngular", "src\AlbumViewerAngular\", "{293C677F-D21F-417A-AC50-7BAB1917AF1C}" 21 | ProjectSection(WebsiteProperties) = preProject 22 | TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0" 23 | Debug.AspNetCompiler.VirtualPath = "/localhost_37063" 24 | Debug.AspNetCompiler.PhysicalPath = "src\AlbumViewerAngular\" 25 | Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_37063\" 26 | Debug.AspNetCompiler.Updateable = "true" 27 | Debug.AspNetCompiler.ForceOverwrite = "true" 28 | Debug.AspNetCompiler.FixedNames = "false" 29 | Debug.AspNetCompiler.Debug = "True" 30 | Release.AspNetCompiler.VirtualPath = "/localhost_37063" 31 | Release.AspNetCompiler.PhysicalPath = "src\AlbumViewerAngular\" 32 | Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_37063\" 33 | Release.AspNetCompiler.Updateable = "true" 34 | Release.AspNetCompiler.ForceOverwrite = "true" 35 | Release.AspNetCompiler.FixedNames = "false" 36 | Release.AspNetCompiler.Debug = "False" 37 | VWDPort = "37063" 38 | SlnRelativePath = "src\AlbumViewerAngular\" 39 | EndProjectSection 40 | EndProject 41 | Global 42 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 43 | Debug|Any CPU = Debug|Any CPU 44 | Release|Any CPU = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 47 | {00D86129-ADE5-403F-96AE-9263C01BE2E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {00D86129-ADE5-403F-96AE-9263C01BE2E4}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {00D86129-ADE5-403F-96AE-9263C01BE2E4}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {00D86129-ADE5-403F-96AE-9263C01BE2E4}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {209EAF20-38E2-4C67-9557-9247A07213E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {209EAF20-38E2-4C67-9557-9247A07213E6}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {209EAF20-38E2-4C67-9557-9247A07213E6}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {209EAF20-38E2-4C67-9557-9247A07213E6}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {4D936F4A-C6BD-4724-810F-949AB4D0BD36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {4D936F4A-C6BD-4724-810F-949AB4D0BD36}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {4D936F4A-C6BD-4724-810F-949AB4D0BD36}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {4D936F4A-C6BD-4724-810F-949AB4D0BD36}.Release|Any CPU.Build.0 = Release|Any CPU 59 | {293C677F-D21F-417A-AC50-7BAB1917AF1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 60 | {293C677F-D21F-417A-AC50-7BAB1917AF1C}.Debug|Any CPU.Build.0 = Debug|Any CPU 61 | {293C677F-D21F-417A-AC50-7BAB1917AF1C}.Release|Any CPU.ActiveCfg = Debug|Any CPU 62 | {293C677F-D21F-417A-AC50-7BAB1917AF1C}.Release|Any CPU.Build.0 = Debug|Any CPU 63 | EndGlobalSection 64 | GlobalSection(SolutionProperties) = preSolution 65 | HideSolutionNode = FALSE 66 | EndGlobalSection 67 | GlobalSection(NestedProjects) = preSolution 68 | {00D86129-ADE5-403F-96AE-9263C01BE2E4} = {0A329D36-19A2-4F54-B3C7-17640DDBF44C} 69 | {209EAF20-38E2-4C67-9557-9247A07213E6} = {0A329D36-19A2-4F54-B3C7-17640DDBF44C} 70 | {4D936F4A-C6BD-4724-810F-949AB4D0BD36} = {0A329D36-19A2-4F54-B3C7-17640DDBF44C} 71 | {293C677F-D21F-417A-AC50-7BAB1917AF1C} = {0A329D36-19A2-4F54-B3C7-17640DDBF44C} 72 | EndGlobalSection 73 | GlobalSection(ExtensibilityGlobals) = postSolution 74 | SolutionGuid = {9FAF684A-A9E1-46AD-8ED6-149D0825737E} 75 | EndGlobalSection 76 | EndGlobal 77 | -------------------------------------------------------------------------------- /AlbumViewer_Docker.websurge: -------------------------------------------------------------------------------- 1 | GET http://localhost:8081/index.html HTTP/1.1 2 | Accept-Encoding: gzip,deflate 3 | 4 | ------------------------------------------------------------------ 5 | 6 | GET http://localhost:8081/api/artist/1 HTTP/1.1 7 | Accept-Encoding: gzip,deflate 8 | 9 | ------------------------------------------------------------------ 10 | 11 | GET http://localhost:8081/api/helloworld HTTP/1.1 12 | Accept-Encoding: gzip,deflate 13 | 14 | ------------------------------------------------------------------ 15 | 16 | 17 | ----- Start WebSurge Options ----- 18 | 19 | { 20 | "NoProgressEvents": true, 21 | "DelayTimeMs": -1, 22 | "NoContentDecompression": false, 23 | "CaptureMinimalResponseData": false, 24 | "MaxResponseSize": 0, 25 | "ReplaceCookieValue": null, 26 | "ReplaceAuthorization": null, 27 | "ReplaceQueryStringValuePairs": null, 28 | "ReplaceDomain": "", 29 | "Username": null, 30 | "Password": null, 31 | "RandomizeRequests": false, 32 | "RequestTimeoutMs": 15000, 33 | "WarmupSeconds": 2, 34 | "ReloadTemplates": false, 35 | "FormattedPreviewTheme": "visualstudio", 36 | "LastThreads": 8, 37 | "IgnoreCertificateErrors": false, 38 | "LastSecondsToRun": 30 39 | } 40 | 41 | // This file was generated by West Wind WebSurge 42 | // Get your free copy at http://websurge.west-wind.com 43 | // to easily test or load test the requests in this file. 44 | 45 | ----- End WebSurge Options ----- 46 | 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Since this project is intended to support a specific use case guide, contributions are limited to bug fixes or security issues. If you have a question, feel free to open an issue! 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Docker Samples 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | 3 | services: 4 | db: 5 | image: dockersamples/tidb:nanoserver-1809 6 | ports: 7 | - "3306:4000" 8 | 9 | app: 10 | image: dockersamples/dotnet-album-viewer 11 | build: 12 | context: . 13 | dockerfile: docker/app/Dockerfile 14 | ports: 15 | - "80:80" 16 | environment: 17 | - "Data:Provider=MySQL" 18 | - "Data:ConnectionString=Server=db;Port=4000;Database=AlbumViewer;User=root;SslMode=None" 19 | depends_on: 20 | - db 21 | 22 | networks: 23 | default: 24 | external: 25 | name: nat 26 | -------------------------------------------------------------------------------- /docker/app/Dockerfile: -------------------------------------------------------------------------------- 1 | # escape=` 2 | FROM mcr.microsoft.com/dotnet/sdk:2.1-nanoserver-1809 AS builder 3 | 4 | WORKDIR /album-viewer 5 | COPY AlbumViewerNetCore.sln . 6 | COPY src/AlbumViewerNetCore/AlbumViewerNetCore.csproj src/AlbumViewerNetCore/AlbumViewerNetCore.csproj 7 | COPY src/AlbumViewerBusiness/AlbumViewerBusiness.csproj src/AlbumViewerBusiness/AlbumViewerBusiness.csproj 8 | COPY src/Westwind.Utilities/Westwind.Utilities.csproj src/Westwind.Utilities/Westwind.Utilities.csproj 9 | RUN dotnet restore src/AlbumViewerNetCore/AlbumViewerNetCore.csproj 10 | 11 | COPY src src 12 | RUN dotnet publish .\src\AlbumViewerNetCore\AlbumViewerNetCore.csproj 13 | 14 | # app image 15 | FROM mcr.microsoft.com/dotnet/aspnet:2.1-nanoserver-1809 16 | 17 | WORKDIR /album-viewer 18 | COPY --from=builder /album-viewer/src/AlbumViewerNetCore/bin/Debug/netcoreapp2.0/publish/ . 19 | CMD ["dotnet", "AlbumViewerNetCore.dll"] 20 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/.angular-cli - Copy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "album-viewer" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "../AlbumViewerNetCore/wwwroot", 10 | "assets": [ 11 | "images", 12 | "favicon.ico", 13 | "touch-icon.png" 14 | ], 15 | "index": "index.html", 16 | "main": "main.ts", 17 | "polyfills": "polyfills.ts", 18 | "test": "test.ts", 19 | "tsconfig": "tsconfig.app.json", 20 | "testTsconfig": "tsconfig.spec.json", 21 | "prefix": "app", 22 | 23 | "styles": [ 24 | "../node_modules/bootstrap/dist/css/bootstrap.css", 25 | "../node_modules/bootstrap/dist/css/bootstrap-theme.css", 26 | "../node_modules/font-awesome/css/font-awesome.css", 27 | "../node_modules/toastr/build/toastr.css", 28 | "./css/albumviewer.css" 29 | ], 30 | "scripts": [ 31 | "../node_modules/jquery/dist/jquery.js", 32 | "../node_modules/toastr/toastr.js", 33 | "../node_modules/bootstrap/dist/js/bootstrap.js", 34 | "../node_modules/bootstrap-3-typeahead/bootstrap3-typeahead.js" 35 | ], 36 | "environmentSource": "environments/environment.ts", 37 | "environments": { 38 | "dev": "environments/environment.ts", 39 | "prod": "environments/environment.prod.ts" 40 | } 41 | } 42 | ], 43 | "e2e": { 44 | "protractor": { 45 | "config": "./protractor.conf.js" 46 | } 47 | }, 48 | "lint": [ 49 | { 50 | "project": "src/tsconfig.app.json", 51 | "exclude": "**/node_modules/**" 52 | }, 53 | { 54 | "project": "src/tsconfig.spec.json", 55 | "exclude": "**/node_modules/**" 56 | }, 57 | { 58 | "project": "e2e/tsconfig.e2e.json", 59 | "exclude": "**/node_modules/**" 60 | } 61 | ], 62 | "test": { 63 | "karma": { 64 | "config": "./karma.conf.js" 65 | } 66 | }, 67 | "defaults": { 68 | "styleExt": "css", 69 | "component": {} 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "album-viewer" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "../AlbumViewerNetCore/wwwroot", 10 | "assets": [ 11 | "images", 12 | "favicon.ico", 13 | "touch-icon.png" 14 | ], 15 | "index": "index.html", 16 | "main": "main.ts", 17 | "polyfills": "polyfills.ts", 18 | "test": "test.ts", 19 | "tsconfig": "tsconfig.app.json", 20 | "testTsconfig": "tsconfig.spec.json", 21 | "prefix": "app", 22 | 23 | "styles": [ 24 | "../node_modules/bootstrap/dist/css/bootstrap.css", 25 | "../node_modules/bootstrap/dist/css/bootstrap-theme.css", 26 | "../node_modules/font-awesome/css/font-awesome.css", 27 | "../node_modules/toastr/build/toastr.css", 28 | "./css/albumviewer.css" 29 | ], 30 | "scripts": [ 31 | "../node_modules/jquery/dist/jquery.js", 32 | "../node_modules/toastr/toastr.js", 33 | "../node_modules/bootstrap/dist/js/bootstrap.js", 34 | "../node_modules/bootstrap-3-typeahead/bootstrap3-typeahead.js" 35 | ], 36 | "environmentSource": "environments/environment.ts", 37 | "environments": { 38 | "dev": "environments/environment.ts", 39 | "prod": "environments/environment.prod.ts" 40 | } 41 | } 42 | ], 43 | "e2e": { 44 | "protractor": { 45 | "config": "./protractor.conf.js" 46 | } 47 | }, 48 | "lint": [ 49 | { 50 | "project": "src/tsconfig.app.json", 51 | "exclude": "**/node_modules/**" 52 | }, 53 | { 54 | "project": "src/tsconfig.spec.json", 55 | "exclude": "**/node_modules/**" 56 | }, 57 | { 58 | "project": "e2e/tsconfig.e2e.json", 59 | "exclude": "**/node_modules/**" 60 | } 61 | ], 62 | "test": { 63 | "karma": { 64 | "config": "./karma.conf.js" 65 | } 66 | }, 67 | "defaults": { 68 | "styleExt": "css", 69 | "component": {} 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | /.idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | *.sublime-workspace 18 | 19 | # IDE - VSCode 20 | .vscode/* 21 | !.vscode/settings.json 22 | !.vscode/tasks.json 23 | !.vscode/launch.json 24 | !.vscode/extensions.json 25 | 26 | # misc 27 | /.sass-cache 28 | /connect.lock 29 | /coverage/* 30 | /libpeerconnection.log 31 | npm-debug.log 32 | testem.log 33 | /typings 34 | 35 | # e2e 36 | /e2e/*.js 37 | /e2e/*.map 38 | 39 | #System Files 40 | .DS_Store 41 | Thumbs.db 42 | package-lock.json 43 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/README.md: -------------------------------------------------------------------------------- 1 | # AlbumViewer 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.0.0-rc.0. 4 | 5 | ## Development server 6 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 7 | 8 | ## Code scaffolding 9 | 10 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`. 11 | 12 | ## Build 13 | 14 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 15 | 16 | ## Running unit tests 17 | 18 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 19 | 20 | ## Running end-to-end tests 21 | 22 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 23 | Before running the tests make sure you are serving the app via `ng serve`. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "album-viewer", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build", 9 | "build:prod": "ng build --prod --aot", 10 | "build:asp": "npm run build:prod & npm run clean & npm run copy", 11 | "test": "ng test", 12 | "lint": "ng lint", 13 | "e2e": "ng e2e", 14 | "copy": "xcopy /s /q .\\dist\\*.* ..\\AlbumViewerNetCore\\wwwroot\\", 15 | "clean": "rd /s /q ..\\AlbumViewerNetCore\\wwwroot\\" 16 | }, 17 | "private": true, 18 | "dependencies": { 19 | "@angular/animations": "^4.0.0", 20 | "@angular/common": "^4.0.0", 21 | "@angular/compiler": "^4.0.0", 22 | "@angular/core": "^4.0.0", 23 | "@angular/forms": "^4.0.0", 24 | "@angular/http": "^4.0.0", 25 | "@angular/platform-browser": "^4.0.0", 26 | "@angular/platform-browser-dynamic": "^4.0.0", 27 | "@angular/router": "^4.0.0", 28 | "core-js": "^2.4.1", 29 | "rxjs": "^5.4.1", 30 | "zone.js": "^0.8.14", 31 | "bootstrap": "^3.3.7", 32 | "bootstrap-3-typeahead": "^4.0.2", 33 | "font-awesome": "^4.7.0", 34 | "jquery": "^3.1.1", 35 | "toastr": "^2.1.2" 36 | }, 37 | "devDependencies": { 38 | "@angular/cli": "^1.0.1", 39 | "@angular/compiler-cli": "^4.1.0", 40 | "@types/jasmine": "2.5.38", 41 | "@types/node": "^6.0.70", 42 | "codelyzer": "~2.0.0", 43 | "jasmine-core": "~2.5.2", 44 | "jasmine-spec-reporter": "~3.2.0", 45 | "karma": "~1.4.1", 46 | "karma-chrome-launcher": "~2.0.0", 47 | "karma-cli": "~1.0.1", 48 | "karma-coverage-istanbul-reporter": "^0.2.0", 49 | "karma-jasmine": "~1.1.0", 50 | "karma-jasmine-html-reporter": "^0.2.2", 51 | "protractor": "~5.1.0", 52 | "ts-node": "~2.0.0", 53 | "tslint": "~4.4.2", 54 | "typescript": "^2.3.1", 55 | "@types/jquery": "^2.0.40", 56 | "@types/toastr": "^2.1.32" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/albums/albumDisplay.html: -------------------------------------------------------------------------------- 1 | 
2 | 3 |
4 | Albums 5 | Edit 6 | Buy 7 | 8 | Listen 9 | 10 |
11 | 12 |
13 | 14 | 15 | 16 |
17 |
18 | 19 |
20 |
21 |

22 | {{album.Title}} 23 |

24 | 25 |
26 | 27 | by {{album.Artist.ArtistName}} 28 | {{(album.Year ? 'in ' + album.Year : '')}} 29 |
30 |
31 | 32 |
33 |   34 | Buy    35 | Play 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 | More from 50 | 51 | {{album.Artist.ArtistName}}
52 | 54 |
55 |
56 |
57 |
58 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/albums/albumDisplay.ts: -------------------------------------------------------------------------------- 1 | import {Album} from './../business/entities'; 2 | import {Component, OnInit, Input, style, animate, state, transition, trigger, OnDestroy} from '@angular/core'; 3 | import {AlbumService} from "./albumService"; 4 | import {ActivatedRoute, Router} from "@angular/router"; 5 | import {ErrorInfo} from "../common/errorDisplay"; 6 | import {AppConfiguration} from "../business/appConfiguration"; 7 | import {slideIn, slideInLeft} from "../common/animations"; 8 | 9 | @Component({ 10 | selector: 'album-display', 11 | templateUrl: './albumDisplay.html', 12 | animations: [ slideIn ] 13 | }) 14 | export class AlbumDisplay implements OnInit { 15 | 16 | @Input() album:Album = new Album(); 17 | error = new ErrorInfo(); 18 | aniFrame = 'in'; 19 | 20 | constructor(private route: ActivatedRoute, 21 | private router: Router, 22 | private config: AppConfiguration, 23 | private albumService: AlbumService) { 24 | } 25 | 26 | ngOnInit() { 27 | this.config.isSearchAllowed = false; 28 | this.config.activeTab = "albums"; 29 | this.aniFrame = 'in'; 30 | 31 | //console.log("AlbumDisplay"); 32 | if(!this.album.Title) { 33 | var id = this.route.snapshot.params["id"]; 34 | if (id < 1) 35 | return; 36 | 37 | this.albumService.getAlbum(id) 38 | .subscribe( result => { 39 | this.album = result; 40 | }, err => this.error.error(err)); 41 | } 42 | } 43 | 44 | ngOnDestroy() { 45 | this.aniFrame = 'out'; 46 | console.log("ngDestroy") 47 | } 48 | 49 | deleteAlbum(album) { 50 | this.albumService.deleteAlbum(album) 51 | .subscribe( result =>{ 52 | this.error.info("Album '" + album.Title + "' has been deleted."); 53 | setTimeout(() => this.router.navigate(["/albums"]), 1500); 54 | }, 55 | (err)=> this.error.error(err)); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/albums/albumEditor.html: -------------------------------------------------------------------------------- 1 | 
2 | 3 |
4 | List 5 | View 6 | Buy 7 |
8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 23 | 24 |
25 |
26 | 27 | 34 | 35 | 36 |
37 |
38 | 39 | 45 | 46 |
47 | 48 |
49 |
50 | 51 | 55 | 56 |
57 |
58 | 59 |
60 |
61 | 62 | 66 |
67 |
68 | 69 |
70 |
71 | 72 | 76 |
77 |
78 | 79 |
80 | 81 | 86 |
87 | 88 |
89 | 90 | 91 | 92 |
93 | 98 | 99 | Cancel 100 | 101 |
102 | 103 |
104 | 105 |
106 |
107 | 108 |
109 |

Preview

110 | 111 |
112 |

{{album.Title}}

113 |
by {{album.Artist.ArtistName}} {{(album.Year ? 'in ' + album.Year : '')}} 114 | - Buy on Amazon 115 |
116 |
117 | 118 |
119 | 120 | 121 | 122 |
123 |
124 | 125 | 126 |
127 | 128 |
129 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/albums/albumEditor.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, ElementRef} from '@angular/core'; 2 | import {Album} from "../business/entities"; 3 | import {AlbumService} from "./albumService"; 4 | import {Router, ActivatedRoute} from "@angular/router"; 5 | import {ErrorInfo} from "../common/errorDisplay"; 6 | import {AppConfiguration} from "../business/appConfiguration"; 7 | import {UserInfo} from "../business/userInfo"; 8 | 9 | //declare var $:any ; 10 | declare var $:any; 11 | declare var toastr:any; 12 | declare var window:any; 13 | 14 | import {slideInLeft, slideIn} from "../common/animations"; 15 | 16 | @Component({ 17 | selector: 'album-editor', 18 | templateUrl: 'albumEditor.html', 19 | animations: [ slideIn ] 20 | }) 21 | export class AlbumEditor implements OnInit { 22 | constructor(private route: ActivatedRoute, 23 | private router: Router, 24 | private albumService: AlbumService, 25 | private config:AppConfiguration, 26 | private user:UserInfo) { 27 | } 28 | 29 | album: Album = new Album(); 30 | error: ErrorInfo = new ErrorInfo(); 31 | loaded = false; 32 | aniFrame = 'in'; 33 | 34 | ngOnInit() { 35 | if (!this.user.isAuthenticated) { 36 | this.router.navigate(['/login']); 37 | return; 38 | } 39 | 40 | this.config.isSearchAllowed = false; 41 | this.bandTypeAhead(); 42 | 43 | 44 | var id = this.route.snapshot.params["id"]; 45 | if (id < 1) { 46 | this.loaded = true; 47 | this.album = this.albumService.newAlbum(); 48 | return; 49 | } 50 | 51 | 52 | 53 | this.albumService.getAlbum(id) 54 | .subscribe(result => { 55 | this.album = result; 56 | this.loaded = true; 57 | }, 58 | err => { 59 | this.error.error(err); 60 | }); 61 | } 62 | 63 | saveAlbum(album) { 64 | return this.albumService.saveAlbum(album) 65 | .subscribe((album: Album) => { 66 | var msg = album.Title + " has been saved." 67 | this.error.info(msg); 68 | toastr.success(msg); 69 | window.document.getElementById("MainView").scrollTop = 0; 70 | 71 | setTimeout(function () { 72 | this.router.navigate(["/album", album.Id]); 73 | }, 1500) 74 | }, 75 | err => { 76 | let msg = `Unable to save album: ${err.message}`; 77 | this.error.error(msg); 78 | toastr.error(msg); 79 | 80 | if (err.response && err.response.status == 401) { 81 | this.user.isAuthenticated = false; 82 | this.router.navigate(["login"]); 83 | } 84 | }); 85 | 86 | }; 87 | 88 | bandTypeAhead() { 89 | var $input:any = $("#BandName"); 90 | var config = this.config; 91 | 92 | // delay slightly to ensure that the 93 | // typeahead component is loaded when 94 | // doing a full browser refresh 95 | setTimeout( function () { 96 | $input.typeahead({ 97 | source: [], 98 | autoselect: true, 99 | minLength: 0 100 | }); 101 | 102 | $input.keyup( function() { 103 | let s = $(this).val(); 104 | let url = config.urls.url("artistLookup") + s; 105 | 106 | $.getJSON(url, 107 | (data) => { 108 | $input.data('typeahead').source = data; 109 | }); 110 | }); 111 | 112 | },1000); 113 | 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/albums/albumList.html: -------------------------------------------------------------------------------- 1 | 
2 | 3 | 4 | 5 | Add Album 6 |
7 | 8 | Albums {{filteredAlbumList.length}} 9 |
10 | 11 |
12 | 14 |
15 | 16 | 17 | 20 | 21 |
22 | 23 | 24 | 25 |   26 | 27 | 28 | 29 |
30 | 31 |
32 | 33 |
34 |
{{album.Title}}
35 |
by {{album.Artist.ArtistName}} {{(album.Year ? 'in ' + album.Year : '')}}
36 |
{{album.Description}}
37 |
38 |
39 | 40 |
41 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/albums/albumList.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit } from '@angular/core'; 2 | import { AlbumService } from './albumService'; 3 | import { Album } from '../business/entities'; 4 | 5 | import {AppConfiguration} from "../business/appConfiguration"; 6 | import {Router} from "@angular/router"; 7 | import {ErrorInfo} from "../common/errorDisplay"; 8 | import {slideIn, slideInLeft} from "../common/animations"; 9 | import { UserInfo } from "../business/userInfo"; 10 | 11 | //import * as $ from 'jquery'; 12 | declare var $:any; 13 | declare var toastr:any; 14 | 15 | 16 | @Component({ 17 | selector: 'album-list', 18 | templateUrl: './albumList.html', 19 | animations: [ slideIn ] 20 | }) 21 | export class AlbumList implements OnInit { 22 | 23 | constructor(private router:Router, 24 | private albumService: AlbumService, 25 | private config: AppConfiguration) { 26 | } 27 | 28 | albumList: Album[] = []; 29 | busy: boolean = true; 30 | error:ErrorInfo = new ErrorInfo(); 31 | 32 | 33 | ngOnInit() { 34 | this.getAlbums(); 35 | 36 | this.config.isSearchAllowed = true; 37 | this.config.activeTab = "albums"; 38 | this.config.searchText = ""; 39 | 40 | // ??? Non-DOM way to do this? 41 | setTimeout(() => { 42 | $("#SearchBox").focus(); 43 | },200); 44 | } 45 | 46 | get filteredAlbumList() { 47 | if (this.config.searchText && this.config.searchText.length > 1) { 48 | var lsearchText = this.config.searchText.toLowerCase(); 49 | return this.albumList.filter((a) => 50 | a.Title.toLowerCase().includes(lsearchText) || 51 | a.Artist.ArtistName.toLowerCase().includes(lsearchText) 52 | ); 53 | } 54 | return this.albumList; 55 | } 56 | 57 | getAlbums() { 58 | this.busy = true; 59 | this.albumList = []; 60 | this.albumService.getAlbums() 61 | .subscribe(albums => { 62 | this.albumList = albums; 63 | this.busy = false; 64 | 65 | // reset scroll position of the list 66 | setTimeout(()=> $("#MainView").scrollTop(this.albumService.listScrollPos), 100); 67 | }, err => { 68 | this.error.error(err); 69 | this.busy = false; 70 | }); 71 | } 72 | 73 | albumClick(album: Album) { 74 | this.albumService.listScrollPos = $("#MainView").scrollTop(); 75 | this.router.navigate(['/album', album.Id]); 76 | } 77 | 78 | 79 | addAlbum() { 80 | 81 | } 82 | 83 | deleteAlbum(album: Album) { 84 | 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/albums/albumService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Album, Artist, Track } from '../business/entities'; 3 | import {AppConfiguration} from "../business/appConfiguration"; 4 | import {ErrorInfo} from "../common/errorDisplay"; 5 | import {Observable} from "rxjs"; 6 | import {Http, RequestOptions} from "@angular/http"; 7 | 8 | 9 | @Injectable() 10 | export class AlbumService { 11 | constructor(private http: Http, 12 | private config:AppConfiguration) { 13 | } 14 | 15 | albumList: Album[] = []; 16 | album: Album = new Album(); 17 | 18 | //artistList: Artist[] = []; 19 | listScrollPos = 0; 20 | 21 | getAlbums(): Observable { 22 | return this.http.get(this.config.urls.url("albums")) 23 | .map( response => { 24 | this.albumList = response.json(); 25 | return this.albumList; 26 | }) 27 | .catch( new ErrorInfo().parseObservableResponseError ); 28 | } 29 | 30 | getAlbum(id):Observable { 31 | return this.http.get(this.config.urls.url("album",id)) 32 | .map( response => { 33 | this.album = response.json(); 34 | 35 | if (!this.albumList || this.albumList.length < 1) 36 | this.getAlbums(); // load up albums in background 37 | 38 | return this.album; 39 | }) 40 | .catch( new ErrorInfo().parseObservableResponseError ); 41 | } 42 | 43 | newAlbum():Album { 44 | this.album = new Album(); 45 | return this.album; 46 | } 47 | saveAlbum(album):Observable { 48 | return this.http.post(this.config.urls.url("album"), 49 | album, 50 | new RequestOptions( {withCredentials:true}) ) 51 | 52 | .map( response => { 53 | this.album = response.json(); 54 | 55 | // explicitly update the list with the updated data 56 | this.updateAlbum(this.album); 57 | 58 | return this.album; 59 | }) 60 | .catch( new ErrorInfo().parseObservableResponseError ); 61 | } 62 | 63 | deleteAlbum(album:Album):Observable { 64 | return this.http.delete(this.config.urls.url("album",album.Id), 65 | this.config.requestOptions) 66 | .map((response)=> { 67 | let result = response.json(); 68 | if (result) 69 | this.removeAlbum(album); 70 | }) 71 | .catch( new ErrorInfo().parsePromiseResponseError ); 72 | } 73 | 74 | 75 | /** 76 | * Updates the .albumList property by updating the actual 77 | * index entry in the existing list, adding new entries and 78 | * removing 0 entries. 79 | * @param album - the album to update 80 | */ 81 | updateAlbum(album) { 82 | var i = this.albumList.findIndex((a) => (a.Id == album.Id)); 83 | if (i > -1) 84 | this.albumList[i] = album; 85 | else { 86 | this.albumList.push(album); 87 | this.albumList.sort((a:Album,b:Album)=> { 88 | var aTitle = a.Title.toLocaleLowerCase(); 89 | var bTitle = b.Title.toLocaleLowerCase(); 90 | if(aTitle > bTitle) 91 | return 1; 92 | if (aTitle a.Id != 0); 99 | } 100 | 101 | removeAlbum(album){ 102 | this.albumList = this.albumList.filter( (a)=> a.Id != album.Id ); 103 | } 104 | addSong(track:Track) { 105 | this.album.Tracks.push(track); 106 | } 107 | 108 | removeSong(track:Track) { 109 | var idx = this.album.Tracks.findIndex((t) => track.Id == t.Id); 110 | if (idx > -1) 111 | this.album.Tracks.splice(idx, 1); 112 | } 113 | 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/albums/albumSongList.html: -------------------------------------------------------------------------------- 1 |
2 | 7 | 8 |
9 | 10 |
11 |
12 | 16 | 17 |
18 |
19 | 23 |
24 |
25 | 26 | 32 | 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 50 | 51 |
{{track.SongName}}{{track.Length}} 45 | 49 |
52 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/albums/albumSongList.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, Input} from '@angular/core'; 2 | import {Track} from "../business/entities"; 3 | import {AlbumService} from "./albumService"; 4 | 5 | @Component({ 6 | //moduleId: module.id, 7 | selector: 'album-songlist', 8 | templateUrl: 'albumSongList.html' 9 | }) 10 | export class AlbumSongList implements OnInit { 11 | constructor(private albumService:AlbumService) { 12 | } 13 | 14 | ngOnInit() { 15 | } 16 | 17 | @Input() tracks: Track[] = []; 18 | @Input() allowEditing: boolean = false; 19 | isSongVisible = false; 20 | track:Track = new Track(); 21 | 22 | addTrack(track){ 23 | this.track = new Track(); 24 | this.isSongVisible = true; 25 | } 26 | cancelTrack() { 27 | this.track = new Track(); 28 | this.isSongVisible = false; 29 | } 30 | 31 | saveTrack(track){ 32 | this.albumService.addSong(track); 33 | this.isSongVisible = false; 34 | } 35 | 36 | removeTrack(track: Track) { 37 | this.albumService.removeSong(track); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { AlbumList } from './albums/albumList'; 5 | import { AlbumDisplay} from './albums/albumDisplay'; 6 | import {AlbumEditor } from './albums/albumEditor'; 7 | import { ArtistList } from './artists/artistList'; 8 | import { ArtistDisplay} from "./artists/artistDisplay"; 9 | import {OptionsComponent} from "./Options/options"; 10 | import {LoginComponent} from "./common/login"; 11 | import {AboutComponent} from "./options/about"; 12 | 13 | const routes: Routes = [ 14 | {path: '', redirectTo: "albums", pathMatch: 'full'}, 15 | {path: '', redirectTo: 'albums', pathMatch: 'full'}, 16 | {path: 'albums', component: AlbumList }, 17 | {path: 'album/:id', component: AlbumDisplay }, 18 | {path: 'album/edit/:id', component: AlbumEditor }, 19 | {path: 'artists', component: ArtistList }, 20 | {path: 'artist/:id', component: ArtistDisplay }, 21 | {path: 'options', component: OptionsComponent }, 22 | { path: 'login', component: LoginComponent }, 23 | { path: 'logout', component: LoginComponent }, 24 | { path: 'about', component: AboutComponent} 25 | ]; 26 | 27 | @NgModule({ 28 | imports: [RouterModule.forRoot(routes)], 29 | exports: [RouterModule], 30 | providers: [] 31 | }) 32 | export class AppRoutingModule { } 33 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/app/app.component.css -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 |
8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 |
16 |
17 | © West Wind Technologies 18 |
19 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | 4 | import { AppComponent } from './app.component'; 5 | 6 | describe('AppComponent', () => { 7 | beforeEach(async(() => { 8 | TestBed.configureTestingModule({ 9 | imports: [ 10 | RouterTestingModule 11 | ], 12 | declarations: [ 13 | AppComponent 14 | ], 15 | }).compileComponents(); 16 | })); 17 | 18 | it('should create the app', async(() => { 19 | const fixture = TestBed.createComponent(AppComponent); 20 | const app = fixture.debugElement.componentInstance; 21 | expect(app).toBeTruthy(); 22 | })); 23 | 24 | it(`should have as title 'app works!'`, async(() => { 25 | const fixture = TestBed.createComponent(AppComponent); 26 | const app = fixture.debugElement.componentInstance; 27 | expect(app.title).toEqual('app works!'); 28 | })); 29 | 30 | it('should render title in a h1 tag', async(() => { 31 | const fixture = TestBed.createComponent(AppComponent); 32 | fixture.detectChanges(); 33 | const compiled = fixture.debugElement.nativeElement; 34 | expect(compiled.querySelector('h1').textContent).toContain('app works!'); 35 | })); 36 | }); 37 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import {UserInfo} from "./business/userInfo"; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.css'] 8 | }) 9 | export class AppComponent { 10 | title = 'Test Application'; 11 | 12 | constructor(private user:UserInfo){ 13 | this.user.checkAuthentication() 14 | .subscribe(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/artists/artistDisplay.html: -------------------------------------------------------------------------------- 1 | 
2 | 3 |
4 | 5 | 6 | Artists 7 | 9 | 10 | Edit 11 | 12 | 13 | 18 |
19 | 20 | 21 | 22 | 23 |

{{artist.ArtistName}}

24 | 25 |
26 | 27 |
28 |
29 | 31 |
32 |
33 |
{{artist.Description }}
34 | 35 | {{artist.ArtistName}} on Amazon 36 |
37 |
38 | 39 | 40 |

Albums

41 |
42 | 43 |
45 |
46 | 47 |   48 | 49 | 50 |
51 | 52 |
53 | 54 |
55 |
{{album.Title}}
56 |
57 | {{(album.Year ? 'in ' + album.Year : '')}} 58 |
59 |
{{album.Description}}
60 |
61 |
62 |
63 | 64 | 65 | 66 | Add Album 67 | 68 | 69 | 70 | 71 |
72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/artists/artistDisplay.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild} from '@angular/core'; 2 | import { ArtistService} from "./artistService"; 3 | import { Artist, Album} from "../business/entities"; 4 | import { AppConfiguration} from "../business/appConfiguration"; 5 | import { ActivatedRoute, Router} from "@angular/router"; 6 | 7 | import {UserInfo} from "../business/userInfo"; 8 | import {ErrorInfo} from "../common/errorDisplay"; 9 | import { ArtistEditor } from "./artistEditor"; 10 | import {slideIn} from "../common/animations"; 11 | 12 | 13 | 14 | @Component({ 15 | //moduleId: module.id, 16 | selector: 'artist-display', 17 | templateUrl: './artistDisplay.html', 18 | animations: [ slideIn ] 19 | }) 20 | export class ArtistDisplay implements OnInit { 21 | // reference a child editor component 22 | @ViewChild(ArtistEditor) editor:ArtistEditor; 23 | 24 | artist: Artist = new Artist(); 25 | albums: Album[] = []; 26 | artistId = null; 27 | formActive = true; 28 | 29 | error:ErrorInfo = new ErrorInfo(); 30 | 31 | constructor(private route: ActivatedRoute, 32 | private artistService: ArtistService, 33 | private config: AppConfiguration, 34 | private router: Router, 35 | private user: UserInfo) { 36 | } 37 | 38 | 39 | ngOnInit() { 40 | this.config.isSearchAllowed = false; 41 | 42 | var id = this.route.snapshot.params["id"]; 43 | if (id < 1) 44 | return; 45 | 46 | this.artistService.getArtist(id) 47 | .subscribe( 48 | (result: any) => { 49 | this.artist = result.Artist; 50 | this.albums = result.Albums; 51 | }, 52 | (err) => { 53 | this.error.error(err); 54 | }); 55 | } 56 | 57 | editArtist() { 58 | if (!this.user.isAuthenticated) { 59 | this.router.navigate(["login"]); 60 | return; 61 | } 62 | 63 | this.editor.showEditor(); 64 | }; 65 | 66 | albumClick(album) { 67 | //window.location.hash = "album/" + album.Id; 68 | this.router.navigate(['/album', album.Id]); 69 | } 70 | 71 | 72 | 73 | addAlbum() { 74 | 75 | } 76 | 77 | deleteArtist(artist:Artist) { 78 | this.artistService.deleteArtist(artist) 79 | .subscribe((result) => { 80 | this.error.info("Album deleted."); 81 | setTimeout(()=> { 82 | this.router.navigate(["/artists"]); 83 | this.artistService.artistList = 84 | this.artistService.artistList.filter( art=> art.Id != artist.Id ); 85 | }, 1200); 86 | }, (err)=> { console.log(err); this.error.error(err) }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/artists/artistEditor.html: -------------------------------------------------------------------------------- 1 | 62 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/artists/artistEditor.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, Input } from '@angular/core'; 2 | import {ArtistService} from "./artistService"; 3 | import {Artist, Album} from "../business/entities"; 4 | 5 | import {ErrorInfo} from "../common/errorDisplay"; 6 | import {AppConfiguration} from "../business/appConfiguration"; 7 | import {UserInfo} from "../business/userInfo"; 8 | 9 | declare var $: any; 10 | 11 | @Component({ 12 | //moduleId: module.id, 13 | selector: 'artist-editor', 14 | templateUrl: 'artistEditor.html' 15 | }) 16 | export class ArtistEditor implements OnInit { 17 | @Input() artist: Artist = new Artist(); 18 | albums: Album[] = []; 19 | formActive = false; 20 | error: ErrorInfo = new ErrorInfo(); 21 | 22 | constructor(private artistService: ArtistService, 23 | private config: AppConfiguration, 24 | private user:UserInfo) { 25 | console.log("ArtistEditor ctor"); 26 | } 27 | 28 | ngOnInit() { 29 | this.config.isSearchAllowed = false; 30 | } 31 | 32 | 33 | showEditor() { 34 | ( $("#EditModal")).modal("show"); 35 | } 36 | 37 | 38 | 39 | saveArtist(artist) { 40 | this.artistService.saveArtist(artist) 41 | .subscribe( result => { 42 | this.artist = result.Artist; 43 | this.albums = result.Albums; 44 | 45 | ( $("#EditModal")).modal("hide"); 46 | 47 | this.formActive = false; 48 | setTimeout(()=> { 49 | this.formActive = true; 50 | }, 0); 51 | 52 | this.error.info("Artist has been saved"); 53 | }, 54 | err => { 55 | this.error.error(err); 56 | 57 | if (err.response && err.response.status == 401) { 58 | window.location.hash="login"; 59 | } 60 | }); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/artists/artistList.html: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/artists/artistList.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import { Artist } from '../business/entities'; 3 | import {AppConfiguration} from "../business/appConfiguration"; 4 | import { ArtistService } from './artistService'; 5 | import {Router} from "@angular/router"; 6 | import {ErrorInfo} from "../common/errorDisplay"; 7 | 8 | import {slideIn, slideInLeft} from "../common/animations"; 9 | 10 | declare var $:any; 11 | 12 | @Component({ 13 | selector: 'artist-list', 14 | templateUrl: 'artistList.html', 15 | animations: [ slideInLeft ] 16 | }) 17 | export class ArtistList implements OnInit { 18 | constructor(private router:Router, 19 | private artistService: ArtistService, 20 | private config: AppConfiguration) { 21 | 22 | } 23 | 24 | artistList: Artist[] = []; 25 | error:ErrorInfo = new ErrorInfo(); 26 | 27 | ngOnInit() { 28 | this.getArtists(); 29 | 30 | this.config.searchText = ""; 31 | this.config.isSearchAllowed = true; 32 | this.config.activeTab = "artists"; 33 | 34 | setTimeout(() => { 35 | $("#SearchBox").focus(); 36 | },200); 37 | } 38 | 39 | get filteredArtistList() { 40 | if(this.config.searchText && this.config.searchText.length > 1) { 41 | var lsearchText = this.config.searchText.toLowerCase(); 42 | return this.artistList.filter((a) => 43 | a.ArtistName.toLowerCase().includes(lsearchText) 44 | ); 45 | } 46 | return this.artistList; 47 | } 48 | 49 | 50 | 51 | 52 | getArtists() { 53 | this.artistService.getArtists() 54 | .subscribe( artists => { 55 | this.artistList = artists; 56 | 57 | setTimeout(() => { 58 | $("#MainView").scrollTop(this.artistService.listScrollPos); 59 | this.artistService.listScrollPos = 0; 60 | }, 20); 61 | return this.artistList; 62 | }, 63 | err => { this.error.error(err) } 64 | ); 65 | } 66 | 67 | artistClick(artist:Artist) { 68 | // Manual Navigation 69 | this.router.navigate(['/artist', artist.Id]); 70 | this.artistService.listScrollPos = $("#MainView").scrollTop(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/artists/artistService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Artist, Album, Track } from '../business/entities'; 3 | 4 | import {AppConfiguration} from "../business/appConfiguration"; 5 | import {Http, RequestOptions} from "@angular/http"; 6 | import {HttpClient} from "../business/HttpClient"; 7 | import {ErrorInfo} from "../common/errorDisplay"; 8 | import {Observable} from "rxjs"; 9 | 10 | 11 | @Injectable() 12 | export class ArtistService { 13 | constructor(private http: Http,private httpClient:HttpClient, 14 | private config: AppConfiguration) { 15 | console.log("ArtistService ctor"); 16 | } 17 | 18 | artistList: Artist[] = []; 19 | artist: Artist = null; 20 | albums: Album[] = []; 21 | error:string = ""; 22 | 23 | listScrollPos = 0; 24 | 25 | 26 | getArtists(force: boolean = false): Observable { 27 | 28 | // use locally cached version 29 | if (force !== true && (this.artistList && this.artistList.length > 0)) 30 | return Observable.of(this.artistList) as Observable; 31 | 32 | return this.http.get(this.config.urls.url("artists")) 33 | .map(response => { 34 | this.artistList = response.json(); 35 | 36 | return this.artistList; 37 | }) 38 | .catch( new ErrorInfo().parseObservableResponseError); 39 | } 40 | 41 | 42 | getArtist(id) { 43 | return this.http.get(this.config.urls.url("artist",id), 44 | this.config.requestOptions) 45 | .map(response => { 46 | var result = response.json(); 47 | this.artist = result.Artist; 48 | this.artist.Albums = result.Albums; 49 | 50 | if(!this.artistList || this.artistList.length < 1) 51 | this.getArtists(); 52 | 53 | return result; 54 | }) 55 | .catch( new ErrorInfo().parseObservableResponseError ); 56 | } 57 | 58 | saveArtist(artist) { 59 | 60 | return this.http.post(this.config.urls.url("saveArtist"),artist, 61 | new RequestOptions( {withCredentials:true} )) 62 | .map( response => { 63 | var result = response.json(); 64 | this.artist = result.Artist; 65 | this.artist.Albums = result.Albums; 66 | 67 | this.updateArtist(result.Artist); 68 | 69 | return result; 70 | }) 71 | .catch( new ErrorInfo().parseObservableResponseError); 72 | } 73 | 74 | // Update the artistList with an artist 75 | updateArtist(artist) { 76 | 77 | var idx = this.artistList.findIndex( (art:Artist) => art.Id == artist.Id ); 78 | 79 | if (idx < 0) 80 | this.artistList.push(artist); 81 | else { 82 | this.artistList[idx] = artist; 83 | } 84 | } 85 | 86 | deleteArtist(artist:Artist) { 87 | return this.http.delete(this.config.urls.url("artist",artist.Id), 88 | new RequestOptions( {withCredentials:true} )) 89 | .map(response => { 90 | return response.json(); // boolean 91 | }) 92 | .catch( new ErrorInfo().parseObservableResponseError); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/business/HttpClient.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from "@angular/core"; 2 | import {Http, RequestOptionsArgs, Response} from "@angular/http"; 3 | import {UserInfo} from "./userInfo"; 4 | import {Observable} from "rxjs"; 5 | 6 | /** 7 | * Wrapper around the Http provider to allow customizing HTTP requests 8 | */ 9 | @Injectable() 10 | export class HttpClient { 11 | 12 | constructor(private http:Http, private user:UserInfo) { 13 | } 14 | 15 | get(url:string, requestOptions?:RequestOptionsArgs) { 16 | this.ensureOptions(!requestOptions); 17 | return this.http 18 | .get(url, requestOptions) 19 | .catch( response => { 20 | if (response.status == 401) 21 | this.user.isAuthenticated = false; 22 | 23 | return Observable.throw(response); 24 | }) 25 | } 26 | 27 | post(url:string, data:any, requestOptions:RequestOptionsArgs) { 28 | this.ensureOptions(!requestOptions) 29 | 30 | return this.http 31 | .post(url, data, requestOptions) 32 | .catch( response => { 33 | if (response.status == 401) 34 | this.user.isAuthenticated = false; 35 | 36 | return Observable.throw(response); 37 | }); 38 | } 39 | 40 | put(url:string, data:any, requestOptions:RequestOptionsArgs) { 41 | //this.ensureOptions(!requestOptions); 42 | 43 | return this.http 44 | .put(url, data, requestOptions) 45 | .catch(response => { 46 | if (response.status == 401) 47 | this.user.isAuthenticated = false; 48 | 49 | return Observable.throw(response); 50 | }); 51 | } 52 | 53 | delete(url:string, requestOptions:RequestOptionsArgs) { 54 | 55 | this.ensureOptions (!requestOptions); 56 | 57 | return this.http.delete(url,requestOptions) 58 | .catch(response => { 59 | if (response.status == 401) 60 | this.user.isAuthenticated = false; 61 | 62 | return Observable.throw(response); 63 | }); 64 | } 65 | 66 | 67 | ensureOptions(requestOptions:RequestOptionsArgs):RequestOptionsArgs { 68 | 69 | 70 | // if (!requestOptions) 71 | // requestOptions = { 72 | // withCredentials: true 73 | // }; 74 | // else 75 | // requestOptions.withCredentials = true; 76 | 77 | return requestOptions; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/business/appConfiguration.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | 4 | import {RequestOptions} from "@angular/http"; 5 | import {ApplicationStats} from "./entities"; 6 | declare var toastr: any; 7 | declare var location: any; 8 | 9 | @Injectable() 10 | export class AppConfiguration { 11 | constructor(){ 12 | this.setToastrOptions(); 13 | console.log("AppConfiguration ctor"); 14 | 15 | if(location.port && (location.port == "3000") || (location.port== "4200") ) 16 | this.urls.baseUrl = "http://localhost:5000/"; // kestrel 17 | 18 | //this.urls.baseUrl = "http://localhost:26448/"; // iis Express 19 | //this.urls.baseUrl = "http://localhost/albumviewer/"; // iis 20 | //this.urls.baseUrl = "https://samples.west-wind.com/AlbumViewerCore/"; // online 21 | } 22 | 23 | // top level search text 24 | searchText = ""; 25 | activeTab = "albums"; 26 | isSearchAllowed = true; 27 | applicationStats:ApplicationStats = new ApplicationStats(); 28 | 29 | urls = { 30 | baseUrl: "./", 31 | //baseUrl: "http://localhost/albumviewer/", 32 | //baseUrl: "http://localhost:5000/", 33 | //baseUrl: "https://albumviewer2swf.west-wind.com/", 34 | albums: "api/albums", 35 | album: "api/album", 36 | artists: "api/artists", 37 | artist: "api/artist", 38 | artistLookup: "api/artistlookup?search=", 39 | saveArtist: "api/artist", 40 | login: "api/login", //"api/login", 41 | logout: "api/logout", 42 | isAuthenticated: "api/isAuthenticated", 43 | reloadData: "api/reloadData", 44 | applicationStats: "api/applicationstats", 45 | url: (name,parm1?,parm2?,parm3?) => { 46 | var url = this.urls.baseUrl + this.urls[name]; 47 | if (parm1) 48 | url += "/" + parm1; 49 | if (parm2) 50 | url += "/" + parm2; 51 | if (parm3) 52 | url += "/" + parm3; 53 | 54 | return url; 55 | } 56 | }; 57 | 58 | 59 | setToastrOptions() { 60 | toastr.options.closeButton = true; 61 | toastr.options.positionClass = "toast-bottom-right"; 62 | } 63 | 64 | /** 65 | * Http Request options to for requests 66 | * @type {RequestOptions} 67 | */ 68 | requestOptions = new RequestOptions({ withCredentials: true }); 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/business/entities.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, InjectionToken} from '@angular/core'; 2 | 3 | @Injectable() 4 | export class Album { 5 | Id:number = 0; 6 | ArtistId:number = 0; 7 | Title:string = null; 8 | Description:string = null; 9 | Year:number = 0; 10 | ImageUrl:string = null; 11 | AmazonUrl:string = null; 12 | SpotifyUrl:string = null; 13 | 14 | Artist:Artist = new Artist(); 15 | Tracks:Track[] = []; 16 | } 17 | 18 | @Injectable() 19 | export class Artist { 20 | Id:number = 0; 21 | ArtistName:string = null; 22 | Description:string = null; 23 | ImageUrl:string = null; 24 | AmazonUrl:string = null; 25 | AlbumCount:number = 0; 26 | Albums:Album[] = []; 27 | } 28 | 29 | 30 | @Injectable() 31 | export class Track { 32 | Id:number = 0; 33 | AlbumId:number = 0; 34 | SongName:string = null; 35 | Length:string = null; 36 | Bytes:number = 0; 37 | UnitPrice:number = 0; 38 | } 39 | 40 | @Injectable() 41 | export class ApplicationStats { 42 | OsPlatform:string = null; 43 | } -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/business/userInfo.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import {Http, RequestOptions} from "@angular/http"; 3 | import {AppConfiguration} from "./appConfiguration"; 4 | import {Observable} from "rxjs"; 5 | import {ErrorInfo} from "../common/errorDisplay"; 6 | 7 | @Injectable() 8 | export class UserInfo { 9 | 10 | isAdmin = false; 11 | userName:string = null; 12 | sessionStarted = new Date(); 13 | 14 | private _isAuthenticated = false; 15 | set isAuthenticated(val) { 16 | this._isAuthenticated = val; 17 | // cache authentication 18 | localStorage.setItem('av_isAuthenticated', val.toString()); 19 | } 20 | get isAuthenticated() { 21 | return this._isAuthenticated; 22 | }; 23 | 24 | 25 | constructor(private http: Http, 26 | private config: AppConfiguration) { 27 | // initialize isAuthenticate from localstorage 28 | var isAuthenticated = localStorage.getItem("av_isAuthenticated"); 29 | this._isAuthenticated = !isAuthenticated || isAuthenticated === 'false' ? false : true; 30 | } 31 | 32 | 33 | login(username, password) { 34 | return this.http.post(this.config.urls.url("login"), { 35 | username: username, 36 | password: password 37 | }, new RequestOptions({withCredentials:true})) 38 | .catch( (response) => { 39 | if(response.status === 401) 40 | this.isAuthenticated = false; 41 | 42 | return new ErrorInfo().parseObservableResponseError(response); 43 | }); 44 | } 45 | 46 | logout() { 47 | return this.http.get(this.config.urls.url("logout"), 48 | new RequestOptions({withCredentials:true})) 49 | .map( 50 | (response) => { 51 | this.isAuthenticated = false; 52 | return true; 53 | } 54 | ); 55 | } 56 | 57 | /** 58 | * Calls to the server to check authentication and then 59 | * updates the local isAuthenticated flag 60 | * @returns {Observable} 61 | */ 62 | checkAuthentication() { 63 | var url = this.config.urls.url( "isAuthenticated" ); 64 | console.log(url); 65 | return this.http.get(url, 66 | new RequestOptions({withCredentials: true})) 67 | .map( (response) => { 68 | let result = response.json(); 69 | this.isAuthenticated = result; 70 | return result; 71 | }) 72 | .catch( (response) => { 73 | this.isAuthenticated = false; 74 | return Observable.throw( response ); 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/common/animations.ts: -------------------------------------------------------------------------------- 1 | import { trigger, state, style, transition, animate } from '@angular/core'; 2 | 3 | export const slideIn = trigger('slideIn', [ 4 | state('*', style({ 5 | transform: 'translateX(100%)', 6 | })), 7 | state('in', style({ 8 | transform: 'translateX(0)', 9 | })), 10 | state('out', style({ 11 | transform: 'translateX(-100%)', 12 | })), 13 | transition('* => in', animate('400ms ease-in')), 14 | transition('in => out', animate('400ms ease-out')) 15 | ]); 16 | 17 | export const slideInLeft = trigger('slideInLeft', [ 18 | state('*', style({ 19 | transform: 'translateX(-100%)', 20 | })), 21 | state('in', style({ 22 | transform: 'translateX(0)', 23 | })), 24 | state('out', style({ 25 | transform: 'translateX(100%)', 26 | })), 27 | transition('* => in', animate('300ms ease-in')), 28 | transition('in => out', animate('300ms ease-out')) 29 | ]); 30 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/common/appFooter.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/common/appFooter.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {AppConfiguration} from "../business/appConfiguration"; 3 | 4 | @Component({ 5 | //moduleId: module.id, 6 | selector: 'app-footer', 7 | templateUrl: 'appFooter.html' 8 | }) 9 | export class AppFooter implements OnInit { 10 | constructor(public config:AppConfiguration) { 11 | 12 | } 13 | 14 | ngOnInit() { 15 | 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/common/appHeader.html: -------------------------------------------------------------------------------- 1 |  11 | 12 | 13 |
14 | 39 |
40 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/common/appHeader.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {AppConfiguration} from "../business/appConfiguration"; 3 | 4 | @Component({ 5 | //moduleId: module.id, 6 | selector: 'app-header', 7 | templateUrl: 'appHeader.html' 8 | }) 9 | export class AppHeader implements OnInit { 10 | constructor(public config:AppConfiguration) { 11 | } 12 | 13 | ngOnInit() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/common/errorDisplay.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, Input } from '@angular/core'; 2 | import { Response} from "@angular/http"; 3 | import {Observable} from "rxjs"; 4 | 5 | /** 6 | * A Bootstrap based alert display 7 | */ 8 | @Component({ 9 | //moduleId: module.id, 10 | selector: 'error-display', 11 | //templateUrl: 'errorDisplay.html' 12 | template: ` 13 |
15 | 19 | 20 |
21 | 22 | {{error.header}} 23 |
24 | 27 | 28 | {{error.message}} 29 |
30 | ` 31 | }) 32 | 33 | export class ErrorDisplay implements OnInit { 34 | constructor() { 35 | } 36 | 37 | /** 38 | * Error object that is bound to the component. 39 | * @type {ErrorInfo} 40 | */ 41 | @Input() error: ErrorInfo = new ErrorInfo(); 42 | 43 | ngOnInit() { } 44 | } 45 | 46 | export class ErrorInfo { 47 | constructor() { 48 | this.reset(); 49 | } 50 | 51 | message:string; 52 | icon:string; 53 | dismissable:boolean; 54 | header:string; 55 | imageIcon:string; 56 | iconColor:string; 57 | 58 | response:Response = null; 59 | 60 | reset() { 61 | this.message = ""; 62 | this.header = ""; 63 | this.dismissable = false; 64 | this.icon = "warning"; 65 | this.imageIcon = "warning"; 66 | this.iconColor = "inherit"; 67 | } 68 | 69 | /** 70 | * Low level method to set message properties 71 | * @param msg - the message to set to 72 | * @param icon? - sets the icon property (warning*) 73 | * @param iconColor? - sets the icon color (left as is) 74 | */ 75 | show(msg:string, icon?:string, iconColor?:string) { 76 | this.message = msg; 77 | this.icon = icon ? icon : "warning"; 78 | if (iconColor) 79 | this.iconColor = iconColor; 80 | 81 | this.fixupIcons(); 82 | 83 | 84 | // if(this.icon == "warning") 85 | // toastr.warning(this.message); 86 | // if(this.icon == "info") 87 | // toastr.info(this.message); 88 | // if (this.icon == "success") 89 | // toastr.success(this.message); 90 | } 91 | 92 | /** 93 | * Displays an error alert 94 | * @param msg - Either a message string or error object with .message property 95 | */ 96 | error(msg) { 97 | if(typeof(msg) === 'object' && msg.message) 98 | this.message = msg.message; 99 | else 100 | this.message = msg; 101 | 102 | this.show(this.message,"warning"); 103 | } 104 | 105 | /** 106 | * DIsplays an info style alert 107 | * @param msg - message to display 108 | */ 109 | info(msg) { 110 | this.show(msg,"info"); 111 | } 112 | 113 | /** 114 | * Fixes up icons and colors based on standard icon settings 115 | * this method is called in internally after any of the helper 116 | * methods are called. You can call this when setting any icon 117 | * related properties manually. 118 | */ 119 | fixupIcons() { 120 | var err = this; 121 | 122 | if (err.icon === "info") 123 | err.imageIcon = "info-circle"; 124 | if (err.icon === "error" || err.icon === "danger" || err.icon === "warning") { 125 | err.imageIcon = "warning"; 126 | err.iconColor = "firebrick"; 127 | } 128 | if (err.icon === "success") { 129 | err.imageIcon = "check"; 130 | err.iconColor = "green"; 131 | } 132 | } 133 | 134 | /** 135 | * Parse a toPromise() .catch() clause error 136 | * from a response object and returns an errorInfo object 137 | * @param response 138 | * @returns {Promise|Promise} 139 | */ 140 | parsePromiseResponseError(response) { 141 | 142 | if (response.hasOwnProperty("message")) 143 | return Promise.reject(response); 144 | if (response.hasOwnProperty("Message")) { 145 | response.message = response.Message; 146 | return Promise.reject(response); 147 | } 148 | 149 | let err = new ErrorInfo(); 150 | err.response = response; 151 | err.message = response.statusText; 152 | 153 | try { 154 | let data = response.json(); 155 | if (data && data.message) 156 | err.message = data.message; 157 | } 158 | catch(ex) { 159 | 160 | } 161 | 162 | return Promise.reject(err); 163 | } 164 | 165 | parseObservableResponseError(response):Observable { 166 | if (response.hasOwnProperty("message")) 167 | return Observable.throw(response); 168 | if (response.hasOwnProperty("Message")) { 169 | response.message = response.Message; 170 | return Observable.throw(response); 171 | } 172 | 173 | let err = new ErrorInfo(); 174 | err.response = response; 175 | err.message = response.statusText; 176 | 177 | try { 178 | let data = response.json(); 179 | if (data && data.message) 180 | err.message = data.message; 181 | } 182 | catch(ex) { } 183 | 184 | if (!err.message) 185 | err.message = "Unknown server failure."; 186 | 187 | return Observable.throw(err); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/common/login.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | AlbumViewer Login 5 |
6 | 7 | 8 | 9 |
10 |
11 | Please sign in 12 |
13 | 14 | 15 |
16 |
17 |
18 |
19 | 20 |
21 | 30 |
31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 | 48 |
49 |
50 |
You can use: uid: test / pwd: test
51 |
52 | 53 | 60 | 61 |
62 | 68 |
69 | 70 |
71 | 72 |
73 | 74 |
75 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/common/login.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {UserInfo} from "../business/userInfo"; 3 | import {ErrorInfo} from "./errorDisplay"; 4 | 5 | declare var toastr:any; 6 | 7 | import {ActivatedRoute, Router} from "@angular/router"; 8 | 9 | @Component({ 10 | //moduleId: module.id, 11 | selector: 'login', 12 | templateUrl: 'login.html' 13 | }) 14 | export class LoginComponent implements OnInit { 15 | username:string = ""; 16 | password:string = ""; 17 | error: ErrorInfo = new ErrorInfo(); 18 | 19 | constructor(public user:UserInfo, 20 | private route:ActivatedRoute, 21 | private router: Router) 22 | { } 23 | 24 | ngOnInit() { 25 | 26 | if (this.route.snapshot.url[0].path == "logout") 27 | this.logout(); 28 | } 29 | 30 | login() { 31 | this.user.login(this.username,this.password) 32 | .subscribe(() => { 33 | this.user.isAuthenticated = true; 34 | toastr.success("You are logged in."); 35 | this.router.navigate(["/albums"]); 36 | }, 37 | (err)=> { 38 | this.error.error(err); 39 | this.password=""; 40 | toastr.warning("Login failed: " + err.message); 41 | }); 42 | } 43 | 44 | logout() { 45 | this.user.logout() 46 | .subscribe((success) => { 47 | toastr.success("Logged out."); 48 | this.router.navigate(["/albums"]); 49 | }); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/options/about.html: -------------------------------------------------------------------------------- 1 |
2 | 8 |
9 | About the AlbumViewer 2 Sample 10 |
11 | 12 |
13 |

ASP.NET Core API backend

14 |

15 | This application runs an ASP.NET Core API server with a 16 | JSON Service backend. 17 |

18 |
19 | 20 |
21 |

Angular 2.0 Front End

22 |

23 | The client interface uses Angular 2.0 to provide the front en logic and 24 | UI management features. There are album and artist services that 25 | talk to the backend interface and page level components and child 26 | components that handle individual view pages of the application. 27 |

28 |
29 | 30 |
31 |

Mobile enabled based on modified Bootstrap Interface

32 |

33 | The base Bootstrap framework is used for the core UI features 34 | of the interface. The base template has been customized for 35 | this application. 36 |

37 |

38 | The application is mobile focused and uses custom resizing 39 | and toolbar relocation based on the screen/device size used. 40 | The application should be comfortably usable from phone up to full screen 41 | desktop applications. 42 |

43 |
44 | 45 |
46 | Back to Application 47 | 48 |
49 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/options/about.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'about', 5 | templateUrl: 'about.html' 6 | }) 7 | export class AboutComponent { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/options/options.html: -------------------------------------------------------------------------------- 1 | 52 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/app/options/options.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {AppConfiguration} from "../business/appConfiguration"; 3 | import {UserInfo} from "../business/userInfo"; 4 | import {ErrorInfo} from "../common/errorDisplay"; 5 | import {Http} from "@angular/http"; 6 | 7 | declare var toastr:any; 8 | declare var window: any; 9 | @Component({ 10 | selector: 'options', 11 | templateUrl: 'options.html' 12 | }) 13 | export class OptionsComponent implements OnInit { 14 | error: ErrorInfo = new ErrorInfo(); 15 | 16 | constructor(public config: AppConfiguration, 17 | public user: UserInfo, 18 | private http: Http) { 19 | } 20 | 21 | ngOnInit() { 22 | this.config.isSearchAllowed = false; 23 | 24 | if (this.config.applicationStats.OsPlatform == null) { 25 | this.http.get(this.config.urls.url("applicationStats")) 26 | .subscribe(response => { 27 | this.config.applicationStats = response.json(); 28 | },response=> { 29 | let obsErr = new ErrorInfo().parseObservableResponseError(response); 30 | let msg = ( obsErr).error.message; 31 | toastr.error("Get Application Stats failed: " + msg); 32 | }); 33 | } 34 | } 35 | 36 | reloadData() { 37 | if (!this.user.isAuthenticated) 38 | window.location.hash = "login"; 39 | 40 | this.http.get(this.config.urls.url("reloadData"), {withCredentials: true}) 41 | .subscribe( 42 | response => { 43 | let success = response.json(); 44 | if (success) 45 | toastr.success("Data has been reloaded."); 46 | else 47 | toastr.error("Unable to reload data"); 48 | }, response => { 49 | let obsErr = new ErrorInfo().parseObservableResponseError(response); 50 | let msg = ( obsErr).error.message; 51 | toastr.error("Data reload failed: " + msg); 52 | 53 | return obsErr; 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/favicon.ico -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/images/RockLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/images/RockLogo.png -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/images/artists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/images/artists.png -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/images/artists32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/images/artists32.png -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/images/gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/images/gear.png -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/images/headphone-head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/images/headphone-head.png -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/images/icon.png -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/images/record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/images/record.png -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/images/search.png -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/images/search_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/images/search_box.png -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/images/search_box2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/images/search_box2.png -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/images/webconnectionicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/images/webconnectionicon.png -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/images/webconnectionlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/images/webconnectionlogo.png -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/images/westwindtext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/images/westwindtext.png -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | West Wind Album Viewer 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | 37 |

Getting things ready...

38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | 39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 41 | 42 | 43 | /** Evergreen browsers require these. **/ 44 | import 'core-js/es6/reflect'; 45 | import 'core-js/es7/reflect'; 46 | 47 | 48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/touch-icon.png -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/touch-icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngular/src/touch-icon32.png -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "es2016", 10 | "dom" 11 | ], 12 | "outDir": "../out-tsc/app", 13 | "target": "es5", 14 | "module": "es2015", 15 | "baseUrl": "", 16 | "types": [] 17 | }, 18 | "exclude": [ 19 | "test.ts", 20 | "**/*.spec.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "es2016" 10 | ], 11 | "outDir": "../out-tsc/spec", 12 | "module": "commonjs", 13 | "target": "es6", 14 | "baseUrl": "", 15 | "types": [ 16 | "jasmine", 17 | "node" 18 | ] 19 | }, 20 | "files": [ 21 | "test.ts" 22 | ], 23 | "include": [ 24 | "**/*.spec.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/AlbumViewerAngular/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2016", 16 | "dom" 17 | ] 18 | } 19 | } -------------------------------------------------------------------------------- /src/AlbumViewerAngular/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "eofline": true, 15 | "forin": true, 16 | "import-blacklist": [ 17 | true, 18 | "rxjs" 19 | ], 20 | "import-spacing": true, 21 | "indent": [ 22 | true, 23 | "spaces" 24 | ], 25 | "interface-over-type-literal": true, 26 | "label-position": true, 27 | "max-line-length": [ 28 | true, 29 | 140 30 | ], 31 | "member-access": false, 32 | "member-ordering": [ 33 | true, 34 | { 35 | "order": [ 36 | "static-field", 37 | "instance-field", 38 | "static-method", 39 | "instance-method" 40 | ] 41 | } 42 | ], 43 | "no-arg": true, 44 | "no-bitwise": true, 45 | "no-console": [ 46 | true, 47 | "debug", 48 | "info", 49 | "time", 50 | "timeEnd", 51 | "trace" 52 | ], 53 | "no-construct": true, 54 | "no-debugger": true, 55 | "no-duplicate-super": true, 56 | "no-empty": false, 57 | "no-empty-interface": true, 58 | "no-eval": true, 59 | "no-inferrable-types": [ 60 | true, 61 | "ignore-params" 62 | ], 63 | "no-misused-new": true, 64 | "no-non-null-assertion": true, 65 | "no-shadowed-variable": true, 66 | "no-string-literal": false, 67 | "no-string-throw": true, 68 | "no-switch-case-fall-through": true, 69 | "no-trailing-whitespace": true, 70 | "no-unnecessary-initializer": true, 71 | "no-unused-expression": true, 72 | "no-use-before-declare": true, 73 | "no-var-keyword": true, 74 | "object-literal-sort-keys": false, 75 | "one-line": [ 76 | true, 77 | "check-open-brace", 78 | "check-catch", 79 | "check-else", 80 | "check-whitespace" 81 | ], 82 | "prefer-const": true, 83 | "quotemark": [ 84 | true, 85 | "single" 86 | ], 87 | "radix": true, 88 | "semicolon": [ 89 | true, 90 | "always" 91 | ], 92 | "triple-equals": [ 93 | true, 94 | "allow-null-check" 95 | ], 96 | "typedef-whitespace": [ 97 | true, 98 | { 99 | "call-signature": "nospace", 100 | "index-signature": "nospace", 101 | "parameter": "nospace", 102 | "property-declaration": "nospace", 103 | "variable-declaration": "nospace" 104 | } 105 | ], 106 | "typeof-compare": true, 107 | "unified-signatures": true, 108 | "variable-name": false, 109 | "whitespace": [ 110 | true, 111 | "check-branch", 112 | "check-decl", 113 | "check-operator", 114 | "check-separator", 115 | "check-type" 116 | ], 117 | "directive-selector": [ 118 | true, 119 | "attribute", 120 | "app", 121 | "camelCase" 122 | ], 123 | "component-selector": [ 124 | true, 125 | "element", 126 | "app", 127 | "kebab-case" 128 | ], 129 | "use-input-property-decorator": true, 130 | "use-output-property-decorator": true, 131 | "use-host-property-decorator": true, 132 | "no-input-rename": true, 133 | "no-output-rename": true, 134 | "use-life-cycle-interface": true, 135 | "use-pipe-transform-interface": true, 136 | "component-class-suffix": true, 137 | "directive-class-suffix": true, 138 | "no-access-missing-member": true, 139 | "templates-use-public": true, 140 | "invoke-injectable": true 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "album-viewer" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "../AlbumViewerNetCore/wwwroot", 10 | "index": "index.html", 11 | "main": "main.ts", 12 | "polyfills": "polyfills.ts", 13 | "test": "test.ts", 14 | "tsconfig": "tsconfig.app.json", 15 | "testTsconfig": "tsconfig.spec.json", 16 | "prefix": "app", 17 | "assets": [ 18 | "images", 19 | "favicon.ico", 20 | "touch-icon.png" 21 | ], 22 | "styles": [ 23 | "../node_modules/bootstrap/dist/css/bootstrap.css", 24 | "../node_modules/bootstrap/dist/css/bootstrap-theme.css", 25 | "../node_modules/font-awesome/css/font-awesome.css", 26 | "../node_modules/toastr/build/toastr.css", 27 | "./css/albumviewer.css" 28 | ], 29 | "scripts": [ 30 | "../node_modules/jquery/dist/jquery.js", 31 | "../node_modules/toastr/toastr.js", 32 | "../node_modules/bootstrap/dist/js/bootstrap.js", 33 | "../node_modules/bootstrap-3-typeahead/bootstrap3-typeahead.js" 34 | ], 35 | "environmentSource": "environments/environment.ts", 36 | "environments": { 37 | "dev": "environments/environment.ts", 38 | "prod": "environments/environment.prod.ts" 39 | } 40 | } 41 | ], 42 | "e2e": { 43 | "protractor": { 44 | "config": "./protractor.conf.js" 45 | } 46 | }, 47 | "lint": [ 48 | { 49 | "project": "src/tsconfig.app.json" 50 | }, 51 | { 52 | "project": "src/tsconfig.spec.json" 53 | }, 54 | { 55 | "project": "e2e/tsconfig.e2e.json" 56 | } 57 | ], 58 | "test": { 59 | "karma": { 60 | "config": "./karma.conf.js" 61 | } 62 | }, 63 | "defaults": { 64 | "styleExt": "css", 65 | "component": {} 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | /.idea 12 | /.vs 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage/* 31 | /libpeerconnection.log 32 | npm-debug.log 33 | testem.log 34 | /typings 35 | 36 | # e2e 37 | /e2e/*.js 38 | /e2e/*.map 39 | 40 | #System Files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/README.md: -------------------------------------------------------------------------------- 1 | # AlbumViewer 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.0.0-rc.0. 4 | 5 | ## Development server 6 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 7 | 8 | ## Code scaffolding 9 | 10 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`. 11 | 12 | ## Build 13 | 14 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 15 | 16 | ## Running unit tests 17 | 18 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 19 | 20 | ## Running end-to-end tests 21 | 22 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 23 | Before running the tests make sure you are serving the app via `ng serve`. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "album-viewer", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/common": "^2.4.0", 16 | "@angular/compiler": "^2.4.0", 17 | "@angular/core": "^2.4.0", 18 | "@angular/forms": "^2.4.0", 19 | "@angular/http": "^2.4.0", 20 | "@angular/platform-browser": "^2.4.0", 21 | "@angular/platform-browser-dynamic": "^2.4.0", 22 | "@angular/router": "^3.4.0", 23 | "bootstrap": "^3.3.7", 24 | "bootstrap-3-typeahead": "^4.0.2", 25 | "core-js": "^2.4.1", 26 | "font-awesome": "^4.7.0", 27 | "jquery": "^3.1.1", 28 | "rxjs": "^5.1.0", 29 | "toastr": "^2.1.2", 30 | "zone.js": "^0.7.6" 31 | }, 32 | "devDependencies": { 33 | "@angular/cli": "1.0.0-rc.0", 34 | "@angular/compiler-cli": "^2.4.0", 35 | "@types/jasmine": "2.5.38", 36 | "@types/node": "~6.0.60", 37 | "codelyzer": "~2.0.0", 38 | "jasmine-core": "~2.5.2", 39 | "jasmine-spec-reporter": "~3.2.0", 40 | "karma": "~1.4.1", 41 | "karma-chrome-launcher": "~2.0.0", 42 | "karma-cli": "~1.0.1", 43 | "karma-jasmine": "~1.1.0", 44 | "karma-jasmine-html-reporter": "^0.2.2", 45 | "karma-coverage-istanbul-reporter": "^0.2.0", 46 | "protractor": "~5.1.0", 47 | "ts-node": "~2.0.0", 48 | "tslint": "~4.4.2", 49 | "typescript": "~2.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/app/albums/albumEditor.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {ActivatedRoute} from "@angular/router"; 3 | import {Http} from "@angular/http"; 4 | import {Album} from "../business/entities"; 5 | 6 | @Component({ 7 | selector: 'album-editor', 8 | templateUrl: 'albumEditor.html' 9 | }) 10 | export class albumEditorComponent implements OnInit { 11 | constructor(private route:ActivatedRoute, 12 | private http:Http) { 13 | } 14 | 15 | album:Album = new Album(); 16 | errorMessage = ""; 17 | baseUrl = "http://localhost:5000/api/"; 18 | 19 | ngOnInit() { 20 | var id = this.route.snapshot.params["id"]; 21 | if (id < 1) 22 | return; 23 | 24 | this.loadAlbum(id); 25 | } 26 | 27 | loadAlbum(id) { 28 | this.errorMessage = ""; 29 | this.http 30 | .get(`${this.baseUrl}album/${id}`) 31 | .subscribe(response => { 32 | this.album = response.json(); 33 | console.log(this.album); 34 | }, response => { 35 | this.errorMessage = "Unable to load album."; 36 | }); 37 | } 38 | 39 | saveAlbum(album) { 40 | return this.http 41 | .post(`${this.baseUrl}album`,album) 42 | .subscribe(response => { 43 | this.album = response.json(); 44 | this.errorMessage = album.Title + " has been saved." 45 | }, 46 | response => { 47 | this.errorMessage = "Unable to save album."; 48 | }); 49 | }; 50 | } 51 | 52 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/app/albums/albumList.html: -------------------------------------------------------------------------------- 1 | 
2 | 3 |
4 | 6 | {{errorMessage}} 7 |
8 | 9 | 11 |
12 | 13 | Albums {{albumList.length}} 14 |
15 | 16 |
17 | 19 |
20 | 21 | 24 | 25 | 26 |
27 |
{{album.Title}}
28 |
29 | by {{album.Artist.ArtistName}} 30 | {{(album.Year ? 'in ' + album.Year : '')}} 31 |
32 |
{{album.Description}}
33 |
34 | 35 |
36 | 37 |
-------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/app/albums/albumList.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {Http} from "@angular/http"; 3 | 4 | @Component({ 5 | selector: 'album-list', 6 | templateUrl: 'albumList.html' 7 | }) 8 | export class albumListComponent { 9 | constructor(private http:Http) { 10 | this.getAlbums(); 11 | } 12 | 13 | albumList = []; 14 | errorMessage:string = null; 15 | busy = false; 16 | 17 | //baseUrl = "http://samples.west-wind.com/AlbumViewerCore/api/"; 18 | baseUrl = "http://localhost:5000/api/"; 19 | 20 | getAlbums() { 21 | this.busy = true; 22 | this.albumList = []; 23 | 24 | var url = this.baseUrl + "albums"; 25 | console.log(url); 26 | this.http.get(url) 27 | .subscribe( (response)=> { 28 | this.albumList = response.json(); 29 | this.busy = false; 30 | },(error)=> { 31 | this.errorMessage = "Request failed."; 32 | }); 33 | } 34 | 35 | 36 | 37 | } 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import {albumListComponent} from "./albums/albumList"; 4 | import {HashLocationStrategy, LocationStrategy} from "@angular/common"; 5 | import {albumEditorComponent} from "./albums/albumEditor"; 6 | 7 | const routes: Routes = [ 8 | {path: '', redirectTo: "albums", pathMatch: 'full'}, 9 | { path: "albums", component: albumListComponent }, 10 | {path: 'album/edit/:id', component: albumEditorComponent }, 11 | //{ path: "albums/:id", component: albumDisplay } 12 | ]; 13 | 14 | @NgModule({ 15 | imports: [RouterModule.forRoot(routes)], 16 | exports: [RouterModule], 17 | providers: [ 18 | // make sure you use this for Hash Urls rather than HTML 5 routing 19 | { 20 | provide: LocationStrategy, 21 | useClass: HashLocationStrategy 22 | } 23 | ] 24 | }) 25 | export class AppRoutingModule { } 26 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/app/app.component.css -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 
2 | 3 | 5 |
6 |
7 | 8 |
9 |
10 |
11 | 12 |
13 | 24 |
25 | 26 |
27 | 28 | 29 |
30 |
31 | © West Wind Technologies 32 |
33 |
34 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | 4 | import { AppComponent } from './app.component'; 5 | 6 | describe('AppComponent', () => { 7 | beforeEach(async(() => { 8 | TestBed.configureTestingModule({ 9 | imports: [ 10 | RouterTestingModule 11 | ], 12 | declarations: [ 13 | AppComponent 14 | ], 15 | }).compileComponents(); 16 | })); 17 | 18 | it('should create the app', async(() => { 19 | const fixture = TestBed.createComponent(AppComponent); 20 | const app = fixture.debugElement.componentInstance; 21 | expect(app).toBeTruthy(); 22 | })); 23 | 24 | it(`should have as title 'app works!'`, async(() => { 25 | const fixture = TestBed.createComponent(AppComponent); 26 | const app = fixture.debugElement.componentInstance; 27 | expect(app.title).toEqual('app works!'); 28 | })); 29 | 30 | it('should render title in a h1 tag', async(() => { 31 | const fixture = TestBed.createComponent(AppComponent); 32 | fixture.detectChanges(); 33 | const compiled = fixture.debugElement.nativeElement; 34 | expect(compiled.querySelector('h1').textContent).toContain('app works!'); 35 | })); 36 | }); 37 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'app works!'; 10 | time = new Date(); 11 | name = 'Rick'; 12 | 13 | interval = setInterval( 14 | () => this.time = new Date(), 15 | 1000); 16 | } 17 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HttpModule } from '@angular/http'; 5 | 6 | import { AppRoutingModule } from './app-routing.module'; 7 | import { AppComponent } from './app.component'; 8 | import {Track, Artist, Album} from "./business/entities"; 9 | import {albumListComponent} from "./albums/albumList"; 10 | import {albumEditorComponent} from "./albums/albumEditor"; 11 | 12 | 13 | @ 14 | NgModule({ 15 | declarations: [ 16 | AppComponent, 17 | albumListComponent, 18 | albumEditorComponent 19 | ], 20 | imports: [ 21 | BrowserModule, 22 | FormsModule, 23 | HttpModule, 24 | AppRoutingModule 25 | ], 26 | providers: [ 27 | Album, 28 | Artist, 29 | Track 30 | ], 31 | bootstrap: [AppComponent] 32 | }) 33 | export class AppModule { } 34 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/app/business/entities.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class Album { 5 | Id:number = 0; 6 | ArtistId:number = 0; 7 | Title:string = null; 8 | Description:string = null; 9 | Year:number = 0; 10 | ImageUrl:string = null; 11 | AmazonUrl:string = null; 12 | SpotifyUrl:string = null; 13 | 14 | Artist:Artist = new Artist(); 15 | Tracks:Track[] = []; 16 | } 17 | 18 | @Injectable() 19 | export class Artist { 20 | Id:number = 0; 21 | ArtistName:string = null; 22 | Description:string = null; 23 | ImageUrl:string = null; 24 | AmazonUrl:string = null; 25 | AlbumCount:number = 0; 26 | Albums:Album[] = []; 27 | } 28 | 29 | @Injectable() 30 | export class Track { 31 | Id:number = 0; 32 | AlbumId:number = 0; 33 | SongName:string = null; 34 | Length:string = null; 35 | Bytes:number = 0; 36 | UnitPrice:number = 0; 37 | 38 | } -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/css/albumviewer.min.css: -------------------------------------------------------------------------------- 1 | body{}.page-header-text{font-size:1.8em;font-weight:bold;color:#4682b4}h2,h3{font-family:'Trebuchet MS','Lucida Sans Unicode','Lucida Grande','Lucida Sans',Arial,sans-serif;font-weight:bold}h3{color:#4682b4}#TitleBar{position:fixed;top:18px;left:8px;z-index:100}#TopMenu{}#SearchBox{width:220px}@media(max-width:500px){#SearchBox{width:135px}}#TopMenu input{color:#535353;padding:4px 5px;height:auto}.banner{background:#535353;color:#e1e1e1;border-bottom:solid 1px #000;position:fixed;top:0;left:0;padding-top:15px;height:58px;width:100%;z-index:10}.banner.active{color:#fff;font-weight:bolder}.banner nav{display:inline-block;margin-top:2px;margin-right:3px;padding:0}.banner nav a,.banner nav input{display:inline-block;padding:7px 5px;color:#e1e1e1;text-decoration:none}.banner nav a:hover{color:#fff;background:#767676;text-decoration:none;border-bottom:solid 4px silver;border-radius:2px}.banner nav a.active,.banner nav a.selected{color:#fff;font-weight:bold;border-bottom:solid 4px #ffa500;border-radius:2px}@media(max-width:640px){.banner nav a span{display:none}}.album{margin-bottom:10px;margin-right:5px;padding:10px;border:1px solid silver;border-radius:6px;height:135px;overflow-y:hidden;float:left;max-width:300px;transition:background linear 275ms;position:relative}.album:hover{background:#4682b4;color:#fff !important;cursor:pointer}.album:hover .album-overlay{display:block}.album-overlay{position:relative;float:right;background:#034e8d;color:#whitesmoke;padding:5px;min-width:40px;display:none;margin-top:-10px;margin-right:-10px;border-radius:3px;opacity:.85;z-index:1000;box-shadow:2px 2px 4px #535353}.album-overlay:hover{opacity:1}.album-overlay a,.album-overlay a:hover{color:#f5f5dc;text-decoration:none}.album-overlay a:hover{color:#fff}.album-title{font-weight:bold}.album-title-big{font-weight:bold;font-size:1.55em}.album-descript{font-size:.85em;overflow:hidden}.album-artist{font-size:.85em;font-style:italic;color:#2a5fb4}.album-year{font-size:.85em;color:#4682b4}.album-image,.album-image-big{width:72px;float:left;border-radius:35px}.album-image-big{width:auto;float:none;max-width:95%;border-radius:4px;box-shadow:2px 2px 4px #535353}@media(max-width:768px){.album-image-big{margin-bottom:10px}}.song{font-size:.85em;padding:5px;border-bottom:1px dashed silver}.separator{border-bottom:1px solid #d3d3d3;margin:5px auto 15px;height:1px}input.ng-invalid,textarea.ng-invalid,select.ng-invalid{background:#ffc0cb}@media(max-width:690px){.album{max-width:100% !important;min-height:100px}}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{border-bottom:solid 2px transparent}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a:hover{text-shadow:0 -1px 0 rgba(0,0,0,.25);color:#fff;background:#535353 !important;border-bottom:solid 2px #ff8c00;border-radius:2px}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a.selected{text-shadow:0 -1px 0 rgba(0,0,0,.25);color:#fff;background:#535353 !important;border-bottom:solid 2px #ffa500;border-radius:2px}.modal-dialog{margin:30px auto}.modal-header{padding:15px 15px 5px}.modal-body{padding:10px 20px 5px 20px}@-webkit-keyframes slideOutLeft{to{-webkit-transform:translateX(-100%);transform:translateX(-100%)}}@keyframes slideOutLeft{to{-webkit-transform:translateX(-100%);transform:translateX(-100%)}}@-webkit-keyframes slideInRight{from{-webkit-transform:translateX(100%);transform:translateX(100%)}to{-webkit-transform:translateX(0%);transform:translateX(0)}}@keyframes slideInRight{from{-webkit-transform:translateX(100%);transform:translateX(100%)}to{-webkit-transform:translateX(0%);transform:translateX(0)}}.slide-animation.ng-enter{-webkit-animation:slideInRight .7s both ease-in;animation:slideInRight .7s both ease-in;z-index:9999}.slide-animation.ng-leave{-webkit-animation:slideOutLeft .35s both ease-in;animation:slideOutLeft .35s both ease-in;z-index:8888}[ng\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak{display:none !important} -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/favicon.ico -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/images/RockLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/images/RockLogo.png -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/images/artists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/images/artists.png -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/images/artists32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/images/artists32.png -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/images/gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/images/gear.png -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/images/headphone-head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/images/headphone-head.png -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/images/icon.png -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/images/record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/images/record.png -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/images/search.png -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/images/search_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/images/search_box.png -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/images/search_box2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/images/search_box2.png -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/images/webconnectionicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/images/webconnectionicon.png -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/images/webconnectionlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/images/webconnectionlogo.png -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/images/westwindtext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/images/westwindtext.png -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AlbumViewer 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | 13 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | 39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 41 | 42 | 43 | /** Evergreen browsers require these. **/ 44 | import 'core-js/es6/reflect'; 45 | import 'core-js/es7/reflect'; 46 | 47 | 48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/touch-icon.png -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/touch-icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerAngularSimple/src/touch-icon32.png -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "es2016", 10 | "dom" 11 | ], 12 | "outDir": "../out-tsc/app", 13 | "target": "es5", 14 | "module": "es2015", 15 | "baseUrl": "", 16 | "types": [] 17 | }, 18 | "exclude": [ 19 | "test.ts", 20 | "**/*.spec.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "es2016" 10 | ], 11 | "outDir": "../out-tsc/spec", 12 | "module": "commonjs", 13 | "target": "es6", 14 | "baseUrl": "", 15 | "types": [ 16 | "jasmine", 17 | "node" 18 | ] 19 | }, 20 | "files": [ 21 | "test.ts" 22 | ], 23 | "include": [ 24 | "**/*.spec.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "lib": [ 11 | "es2016" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/AlbumViewerAngularSimple/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [true, "rxjs"], 16 | "import-spacing": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-over-type-literal": true, 22 | "label-position": true, 23 | "max-line-length": [ 24 | true, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | "static-before-instance", 31 | "variables-before-functions" 32 | ], 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-construct": true, 44 | "no-debugger": true, 45 | "no-duplicate-variable": true, 46 | "no-empty": false, 47 | "no-empty-interface": true, 48 | "no-eval": true, 49 | "no-inferrable-types": [true, "ignore-params"], 50 | "no-shadowed-variable": true, 51 | "no-string-literal": false, 52 | "no-string-throw": true, 53 | "no-switch-case-fall-through": true, 54 | "no-trailing-whitespace": true, 55 | "no-unused-expression": true, 56 | "no-use-before-declare": true, 57 | "no-var-keyword": true, 58 | "object-literal-sort-keys": false, 59 | "one-line": [ 60 | true, 61 | "check-open-brace", 62 | "check-catch", 63 | "check-else", 64 | "check-whitespace" 65 | ], 66 | "prefer-const": true, 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "radix": true, 72 | "semicolon": [ 73 | "always" 74 | ], 75 | "triple-equals": [ 76 | true, 77 | "allow-null-check" 78 | ], 79 | "typedef-whitespace": [ 80 | true, 81 | { 82 | "call-signature": "nospace", 83 | "index-signature": "nospace", 84 | "parameter": "nospace", 85 | "property-declaration": "nospace", 86 | "variable-declaration": "nospace" 87 | } 88 | ], 89 | "typeof-compare": true, 90 | "unified-signatures": true, 91 | "variable-name": false, 92 | "whitespace": [ 93 | true, 94 | "check-branch", 95 | "check-decl", 96 | "check-operator", 97 | "check-separator", 98 | "check-type" 99 | ], 100 | 101 | "directive-selector": [true, "attribute", "app", "camelCase"], 102 | "component-selector": [true, "element", "app", "kebab-case"], 103 | "use-input-property-decorator": true, 104 | "use-output-property-decorator": true, 105 | "use-host-property-decorator": true, 106 | "no-input-rename": true, 107 | "no-output-rename": true, 108 | "use-life-cycle-interface": true, 109 | "use-pipe-transform-interface": true, 110 | "component-class-suffix": true, 111 | "directive-class-suffix": true, 112 | "no-access-missing-member": true, 113 | "templates-use-public": true, 114 | "invoke-injectable": true 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/AlbumViewerBusiness/AccountRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Westwind.BusinessObjects; 8 | 9 | namespace AlbumViewerBusiness 10 | { 11 | /// 12 | /// Account repository used to validate and manage user accounts 13 | /// 14 | 15 | public class AccountRepository : EntityFrameworkRepository 16 | { 17 | public AccountRepository(AlbumViewerContext context) 18 | : base(context) 19 | { } 20 | 21 | public async Task Authenticate(string username, string password) 22 | { 23 | // TODO: Do proper password hashing - for now DEMO CODE 24 | // var hashedPassword = AppUtils.HashPassword(password); 25 | var hashedPassword = password; 26 | 27 | var user = await Context.Users.FirstOrDefaultAsync(usr => 28 | usr.Username == username && 29 | usr.Password == hashedPassword); 30 | if (user == null) 31 | return false; 32 | 33 | return true; 34 | } 35 | 36 | public async Task AuthenticateAndLoadUser(string username, string password) 37 | { 38 | // TODO: Do proper password hashing - for now DEMO CODE 39 | // var hashedPassword = AppUtils.HashPassword(password); 40 | var hashedPassword = password; 41 | 42 | var user = await Context.Users 43 | .FirstOrDefaultAsync(usr => usr.Username == username && 44 | usr.Password == hashedPassword); 45 | return user; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/AlbumViewerBusiness/AlbumViewerBusiness.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/AlbumViewerBusiness/ArtistRepository.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.EntityFrameworkCore; 7 | using Westwind.BusinessObjects; 8 | using Westwind.Utilities; 9 | 10 | namespace AlbumViewerBusiness 11 | { 12 | public class ArtistRepository : EntityFrameworkRepository 13 | { 14 | public ArtistRepository(AlbumViewerContext context) 15 | : base(context) 16 | { } 17 | 18 | public async Task> GetAllArtists() 19 | { 20 | return await Context.Artists 21 | .OrderBy(art => art.ArtistName) 22 | .Select(art => new ArtistWithAlbumCount() 23 | { 24 | ArtistName = art.ArtistName, 25 | Description = art.Description, 26 | ImageUrl = art.ImageUrl, 27 | Id = art.Id, 28 | AmazonUrl = art.AmazonUrl, 29 | AlbumCount = Context.Albums.Count(alb => alb.ArtistId == art.Id) 30 | }) 31 | .ToListAsync(); 32 | } 33 | 34 | /// 35 | /// Returns a list of albums for a given artist 36 | /// 37 | /// 38 | /// 39 | public async Task> GetAlbumsForArtist(int artistId) 40 | { 41 | return await Context.Albums 42 | .Include(a => a.Tracks) 43 | .Include(a => a.Artist) 44 | .Where(a => a.ArtistId == artistId) 45 | .ToListAsync(); 46 | } 47 | 48 | 49 | /// 50 | /// Artist look up by name - used for auto-complete box returns 51 | /// 52 | /// 53 | /// 54 | public async Task> ArtistLookup(string search = null) 55 | { 56 | if (string.IsNullOrEmpty(search)) 57 | return new List(); 58 | 59 | var repo = new AlbumRepository(Context); 60 | 61 | var term = search.ToLower(); 62 | return await repo.Context.Artists 63 | .Where(a => a.ArtistName.ToLower().StartsWith(term)) 64 | .Select(a => new ArtistLookupItem 65 | { 66 | name = a.ArtistName, 67 | id = a.ArtistName 68 | }) 69 | .ToListAsync(); 70 | } 71 | 72 | 73 | 74 | public async Task DeleteArtist(int id) 75 | { 76 | bool result = false; 77 | using (var tx = Context.Database.BeginTransaction()) 78 | { 79 | var artist = await Context.Artists.FirstOrDefaultAsync(art => art.Id == id); 80 | 81 | // already gone 82 | if (artist == null) 83 | return true; 84 | 85 | var albumIds = await Context.Albums.Where(alb => alb.ArtistId == id).Select(alb => alb.Id).ToListAsync(); 86 | 87 | var albumRepo = new AlbumRepository(Context); 88 | 89 | foreach (var albumId in albumIds) 90 | { 91 | // don't run async or we get p 92 | result = await albumRepo.DeleteAlbum(albumId); 93 | if (!result) 94 | return false; 95 | } 96 | 97 | Context.Artists.Remove(artist); 98 | 99 | result = await SaveAsync(); // just save 100 | if (!result) 101 | return false; 102 | 103 | tx.Commit(); 104 | 105 | return result; 106 | } 107 | } 108 | 109 | protected override bool OnValidate(Artist entity) 110 | { 111 | if (entity == null) 112 | { 113 | ValidationErrors.Add("No artist to validate."); 114 | return false; 115 | } 116 | 117 | if (string.IsNullOrEmpty(entity.ArtistName)) 118 | ValidationErrors.Add("Please enter a artist name.","AritistName"); 119 | else if (string.IsNullOrEmpty(entity.Description) || entity.Description.Length < 30) 120 | ValidationErrors.Add("Description must be at least 30 characters.", "Description"); 121 | 122 | return ValidationErrors.Count == 0; 123 | } 124 | 125 | } 126 | 127 | public class ArtistLookupItem 128 | { 129 | public string name { get; set; } 130 | public string id { get; set; } 131 | } 132 | } -------------------------------------------------------------------------------- /src/AlbumViewerBusiness/Models/AlbumViewerContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System; 3 | 4 | 5 | namespace AlbumViewerBusiness 6 | { 7 | public class AlbumViewerContext : DbContext 8 | { 9 | public string ConnectionString { get; set; } 10 | 11 | public AlbumViewerContext(DbContextOptions options) : base(options) 12 | { 13 | } 14 | 15 | public DbSet Albums { get; set; } 16 | public DbSet Artists { get; set; } 17 | public DbSet Tracks { get; set; } 18 | public DbSet Users { get; set; } 19 | 20 | 21 | protected override void OnModelCreating(ModelBuilder builder) 22 | { 23 | base.OnModelCreating(builder); 24 | } 25 | 26 | 27 | //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 28 | //{ 29 | // base.OnConfiguring(optionsBuilder); 30 | 31 | // if (optionsBuilder.IsConfigured) 32 | // return; 33 | 34 | // // Auto configuration 35 | // ConnectionString = Configuration.GetValue("Data:AlbumViewer:ConnectionString"); 36 | // optionsBuilder.UseSqlServer(ConnectionString); 37 | //} 38 | 39 | } 40 | } -------------------------------------------------------------------------------- /src/AlbumViewerBusiness/Models/AlbumViewerDataImporter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace AlbumViewerBusiness 8 | { 9 | 10 | /// 11 | /// This class imports Albums, artists and tracks from the 12 | /// wwwroot/data/albums.js file which contains all the data 13 | /// in a single graph. 14 | /// 15 | public class AlbumViewerDataImporter 16 | { 17 | public static bool EnsureAlbumData(AlbumViewerContext context, string jsonDataFilePath) 18 | { 19 | bool hasData = false; 20 | try 21 | { 22 | hasData = context.Albums.Any(); 23 | } 24 | catch 25 | { 26 | context.Database.EnsureCreated(); // just create the schema as is no migrations 27 | hasData = context.Albums.Any(); 28 | } 29 | 30 | 31 | if (!hasData) 32 | { 33 | string json = System.IO.File.ReadAllText(jsonDataFilePath); 34 | return ImportFromJson(context, json) > 0; 35 | } 36 | 37 | 38 | return true; 39 | } 40 | 41 | /// 42 | /// Imports data from json 43 | /// 44 | /// 45 | /// 46 | public static int ImportFromJson(AlbumViewerContext context, string json) 47 | { 48 | var albums = JsonConvert.DeserializeObject(json); 49 | 50 | foreach (var album in albums) 51 | { 52 | // clear out primary/identity keys so insert works 53 | album.Id = 0; 54 | album.ArtistId = 0; 55 | album.Artist.Id = 0; 56 | 57 | var existingArtist = context.Artists.Where(a => a.ArtistName == album.Artist.ArtistName).FirstOrDefault(); 58 | if (existingArtist == null) 59 | { 60 | context.Artists.Add(album.Artist); 61 | } 62 | else 63 | { 64 | album.Artist = existingArtist; 65 | album.ArtistId = existingArtist.Id; 66 | } 67 | 68 | if (album.Tracks != null) 69 | { 70 | foreach (var track in album.Tracks) 71 | { 72 | track.Id = 0; 73 | context.Add(track); 74 | } 75 | } 76 | context.Add(album); 77 | 78 | try 79 | { 80 | context.SaveChanges(); 81 | } 82 | catch 83 | { 84 | Console.WriteLine("Error adding: " + album.ArtistId); 85 | } 86 | } 87 | 88 | var user = new User() 89 | { 90 | Username = "test", 91 | Password = "test", 92 | Fullname = "Test User", 93 | }; 94 | context.Users.Add(user); 95 | context.SaveChanges(); 96 | 97 | return 1; 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /src/AlbumViewerBusiness/Models/AlbumViewerEntities.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace AlbumViewerBusiness 7 | { 8 | public class Album 9 | { 10 | public int Id { get; set; } 11 | public int ArtistId { get; set; } 12 | public string Title { get; set; } 13 | public string Description { get; set; } 14 | public int Year { get; set; } 15 | public string ImageUrl { get; set; } 16 | public string AmazonUrl { get; set; } 17 | public string SpotifyUrl { get; set; } 18 | 19 | public virtual Artist Artist { get; set; } 20 | public virtual IList Tracks { get; set; } 21 | 22 | public Album() 23 | { 24 | Artist = new Artist(); 25 | Tracks = new List(); 26 | } 27 | 28 | } 29 | 30 | public class Artist 31 | { 32 | public int Id { get; set; } 33 | public string ArtistName { get; set; } 34 | public string Description { get; set; } 35 | public string ImageUrl { get; set; } 36 | public string AmazonUrl { get; set; } 37 | 38 | //public List Albums { get; set; } 39 | } 40 | 41 | public class ArtistWithAlbumCount : Artist 42 | { 43 | public int AlbumCount { get; set; } 44 | } 45 | 46 | public class Track 47 | { 48 | public int Id { get; set; } 49 | public int AlbumId { get; set; } 50 | public string SongName { get; set; } 51 | public string Length { get; set; } 52 | public int Bytes { get; set; } 53 | public decimal UnitPrice { get; set; } 54 | 55 | public override string ToString() 56 | { 57 | return SongName; 58 | } 59 | } 60 | 61 | public class User 62 | { 63 | public int Id { get; set; } 64 | 65 | public string Username { get; set; } 66 | 67 | public string Password { get; set; } 68 | 69 | public string Fullname { get; set; } 70 | } 71 | } -------------------------------------------------------------------------------- /src/AlbumViewerBusiness/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 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: AssemblyTrademark("")] 9 | 10 | // Setting ComVisible to false makes the types in this assembly not visible 11 | // to COM components. If you need to access a type in this assembly from 12 | // COM, set the ComVisible attribute to true on that type. 13 | [assembly: ComVisible(false)] 14 | 15 | // The following GUID is for the ID of the typelib if this project is exposed to COM 16 | [assembly: Guid("209eaf20-38e2-4c67-9557-9247a07213e6")] 17 | -------------------------------------------------------------------------------- /src/AlbumViewerBusiness/Westwind.BusinessObjects/Utilities/DbSetExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Threading.Tasks; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.EntityFrameworkCore.Infrastructure; 8 | using Microsoft.EntityFrameworkCore.Internal; 9 | using System.Reflection; 10 | using Microsoft.EntityFrameworkCore.Metadata.Internal; 11 | 12 | namespace Westwind.BusinessObjects 13 | { 14 | 15 | /// 16 | /// Add missing Find() method to dbContext 17 | /// 18 | public static class DbSetExtensions 19 | { 20 | 21 | /// 22 | /// Manual implementation of Find() 23 | /// 24 | /// 25 | /// 26 | /// 27 | /// 28 | /// 29 | /// based on post: https://weblogs.asp.net/ricardoperes/implementing-missing-features-in-entity-framework-core?__r=8d3ec9c92c78bf8 30 | /// 31 | public static TEntity Find(this DbSet set, params object[] keyValues) where TEntity : class 32 | { 33 | // This doesn't work 34 | //var svcs = set.GetInfrastructure().GetService(); 35 | ///var context = svcs.CurrentContext.Context; 36 | 37 | 38 | var prop = set.GetType() 39 | .GetField("_context", BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance); 40 | 41 | var context = prop 42 | .GetValue(set) as DbContext; 43 | 44 | var entityType = context.Model.FindEntityType(typeof(TEntity)); 45 | var keys = entityType.GetKeys(); 46 | var entries = context.ChangeTracker.Entries(); 47 | var parameter = Expression.Parameter(typeof(TEntity), "x"); 48 | IQueryable query = context.Set(); 49 | 50 | //first, check if the entity exists in the cache 51 | var i = 0; 52 | 53 | //iterate through the key properties 54 | foreach (var property in keys.SelectMany(x => x.Properties)) 55 | { 56 | var keyValue = keyValues[i]; 57 | 58 | //try to get the entity from the local cache 59 | entries = entries.Where(e => keyValue.Equals(e.Property(property.Name).CurrentValue)); 60 | 61 | //build a LINQ expression for loading the entity from the store 62 | var expression = Expression.Lambda( 63 | Expression.Equal( 64 | Expression.Property(parameter, property.Name), 65 | Expression.Constant(keyValue)), 66 | parameter) as Expression>; 67 | 68 | query = query.Where(expression); 69 | 70 | i++; 71 | } 72 | 73 | var entity = entries.Select(x => x.Entity).FirstOrDefault(); 74 | if (entity != null) 75 | return entity; 76 | 77 | //second, try to load the entity from the data store 78 | entity = query.FirstOrDefault(); 79 | 80 | return entity; 81 | } 82 | 83 | /// 84 | /// Returns an entity key for a give entity 85 | /// 86 | /// 87 | /// 88 | /// 89 | /// 90 | public static object[] GetEntityKey(this DbContext context, T entity) where T : class 91 | { 92 | var state = context.Entry(entity); 93 | var metadata = state.Metadata; 94 | var key = metadata.FindPrimaryKey(); 95 | var props = key.Properties.ToArray(); 96 | 97 | return props.Select(x => x.GetGetter().GetClrValue(entity)).ToArray(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = tab 3 | indent_size = 4 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | end_of_line = crlf 8 | # editorconfig-tools is unable to ignore longs strings or urls 9 | max_line_length = null 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/AlbumViewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/AlbumViewer.png -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/AlbumViewerNetCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.0 4 | true 5 | true 6 | $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; 7 | 8 | 9 | 10 | 11 | 12 | PreserveNewest 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Controllers/AccountController.cs: -------------------------------------------------------------------------------- 1 | using AlbumViewerBusiness; 2 | using System.Threading.Tasks; 3 | using System.Security.Claims; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Authentication.Cookies; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Authentication; 8 | using Microsoft.AspNetCore.Cors; 9 | 10 | namespace AlbumViewerAspNetCore 11 | { 12 | [ServiceFilter(typeof(ApiExceptionFilter))] 13 | [EnableCors("CorsPolicy")] 14 | public class AccountController : Controller 15 | { 16 | private AccountRepository accountRepo; 17 | 18 | public AccountController(AccountRepository actRepo) 19 | { 20 | accountRepo = actRepo; 21 | } 22 | 23 | 24 | [AllowAnonymous] 25 | [HttpPost] 26 | [Route("api/login")] 27 | public async Task Login([FromBody] User loginUser) 28 | { 29 | var user = await accountRepo.AuthenticateAndLoadUser(loginUser.Username, loginUser.Password); 30 | if (user == null) 31 | throw new ApiException("Invalid Login Credentials", 401); 32 | 33 | var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); 34 | identity.AddClaim(new Claim(ClaimTypes.Name, user.Username)) ; 35 | 36 | if (user.Fullname == null) 37 | user.Fullname = string.Empty; 38 | identity.AddClaim(new Claim("FullName", user.Fullname)); 39 | 40 | 41 | //context.Authenticate | Challenge | SignInAsync("scheme"); // Calls 2.0 auth stack 42 | 43 | await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, 44 | new ClaimsPrincipal(identity)); 45 | 46 | return true; 47 | } 48 | 49 | [AllowAnonymous] 50 | [HttpGet] 51 | [Route("api/logout")] 52 | public async Task Logout() 53 | { 54 | await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); 55 | return true; 56 | } 57 | 58 | [HttpGet] 59 | [Route("api/isAuthenticated")] 60 | public bool IsAuthenthenticated() 61 | { 62 | return User.Identity.IsAuthenticated; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Controllers/AlbumViewerMvcController.NOTUSED: -------------------------------------------------------------------------------- 1 | #if false 2 | using System.Runtime; 3 | using AlbumViewerBusiness; 4 | using System; 5 | using System.Linq; 6 | 7 | using System.Reflection; 8 | using System.Threading.Tasks; 9 | using AlbumViewerAspNet5; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.EntityFrameworkCore; 12 | 13 | // For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 14 | 15 | namespace MusicStoreVNext 16 | { 17 | public class AlbumViewerMvcController : Controller 18 | { 19 | AlbumViewerContext context; 20 | private readonly IServiceProvider serviceProvider; 21 | 22 | public AlbumViewerMvcController(AlbumViewerContext ctx, IServiceProvider svcProvider) 23 | { 24 | context = ctx; 25 | serviceProvider = svcProvider; 26 | //this.environment = environment; 27 | } 28 | 29 | 30 | public async Task Index() 31 | { 32 | return await Albums(); 33 | } 34 | 35 | [Route("albums")] 36 | public async Task Albums() 37 | { 38 | 39 | var result = await context.Albums 40 | .Include(ctx => ctx.Tracks) 41 | .Include(ctx => ctx.Artist) 42 | .OrderBy(alb => alb.Title) 43 | .ToListAsync(); 44 | 45 | return View("Albums", result); 46 | } 47 | 48 | 49 | [Route("album/{id:int}")] 50 | public async Task Album(int id) 51 | { 52 | var albumRepo = new AlbumRepository(context); 53 | var album = await albumRepo.Load(id); 54 | 55 | return View("Album", album); 56 | } 57 | 58 | [Route("artists")] 59 | public async Task Artists() 60 | { 61 | var artists = await context.Artists 62 | .OrderBy(art => art.ArtistName) 63 | .Select(art => new ArtistWithAlbumCount() 64 | { 65 | ArtistName = art.ArtistName, 66 | Description = art.Description, 67 | ImageUrl = art.ImageUrl, 68 | Id = art.Id, 69 | AmazonUrl = art.AmazonUrl, 70 | AlbumCount = context.Albums.Count(alb => alb.ArtistId == art.Id) 71 | }) 72 | .ToAsyncEnumerable() 73 | .ToList(); 74 | 75 | return View("Artists", artists); 76 | } 77 | 78 | 79 | 80 | [HttpGet,HttpPost] 81 | public ActionResult TagHelpers(Album album = null) 82 | { 83 | if (album == null || (string.IsNullOrEmpty(album.Description) && string.IsNullOrEmpty(album.Title))) 84 | { 85 | album = new Album() 86 | { 87 | Id = 1, 88 | Title = "Highway to Hell", 89 | Description = "AC/DC's best" 90 | }; 91 | } 92 | else 93 | { 94 | 95 | if (ModelState.IsValid) 96 | { 97 | ModelState.Remove("Title"); 98 | album.Title = album.Title + " updated."; 99 | } 100 | } 101 | 102 | return View(album); 103 | } 104 | 105 | 106 | 107 | } 108 | } 109 | #endif -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Controllers/HelloWorldController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | // For more information on enabling Web API for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 8 | 9 | namespace AlbumViewerNetCore.Controllers 10 | { 11 | public class HelloWorldController : Controller 12 | { 13 | 14 | [HttpGet] 15 | [Route("index2.html")] 16 | public string Index() 17 | { 18 | return "

Hello


Index page test"; 19 | } 20 | 21 | [HttpGet] 22 | [Route("api/helloworld")] 23 | public object HelloWorld(string name = null) 24 | { 25 | if (string.IsNullOrEmpty(name)) 26 | name = "Johnny Doe"; 27 | 28 | return new 29 | { 30 | helloMessage = "Hello " + name, 31 | Time = DateTime.Now 32 | }; 33 | } 34 | 35 | 36 | [HttpGet("api/applicationstats")] 37 | public object GetApplicationStats() 38 | { 39 | var stats = new 40 | { 41 | OsPlatform = System.Runtime.InteropServices.RuntimeInformation.OSDescription, 42 | HostName = System.Environment.MachineName, 43 | Ip = HttpContext.Connection.LocalIpAddress.ToString() 44 | }; 45 | 46 | return stats; 47 | } 48 | 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace AlbumViewerAspNet5 8 | { 9 | public class HomeController : Controller 10 | { 11 | public IActionResult Index() 12 | { 13 | return Redirect("index.html"); 14 | } 15 | 16 | public IActionResult About() 17 | { 18 | ViewBag.Message = "Your application description page."; 19 | 20 | return View(); 21 | } 22 | 23 | public IActionResult Contact() 24 | { 25 | ViewBag.Message = "Your contact page..."; 26 | 27 | return View("~/views/home/contact.cshtml"); 28 | } 29 | 30 | public string ThrowError() 31 | { 32 | string name = null; 33 | return name.ToLower(); 34 | } 35 | 36 | public IActionResult Error() 37 | { 38 | return View("~/Views/Shared/Error.cshtml"); 39 | } 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Controllers/TagHelpers/ErrorDisplayTagHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | 3 | namespace AlbumViewerAspNet5.Controllers.TagHelpers 4 | { 5 | 6 | /// 7 | /// TagHelper that display a bootstrap alert window conditionally 8 | /// if a message or header are set on this helper. Otherwise - nothing 9 | /// displays. 10 | /// 11 | [HtmlTargetElement("error-display")] 12 | public class ErrorDisplayTagHelper : TagHelper 13 | { 14 | /// 15 | /// the main message that gets displayed 16 | /// 17 | [HtmlAttributeName("message")] 18 | public string message { get; set; } 19 | 20 | /// 21 | /// Optional header that is displayed in big text. Use for 22 | /// 'noisy' warnings and stop errors only please :-) 23 | /// The message is displayed below the header. 24 | /// 25 | [HtmlAttributeName("header")] 26 | public string header { get; set; } 27 | 28 | /// 29 | /// Font-awesome icon name without the fa- prefix. 30 | /// Example: info-circle, warning, lightbulb-o 31 | /// If none is specified - warning is used 32 | /// 33 | [HtmlAttributeName("icon")] 34 | public string icon { get; set; } 35 | 36 | /// 37 | /// CSS class. Handled here so we can capture the existing 38 | /// class value and append the BootStrap alert class. 39 | /// 40 | [HtmlAttributeName("class")] 41 | public string cssClass { get; set; } 42 | 43 | /// 44 | /// Optional - specifies the alert class used on the top level 45 | /// window. If not specified uses the same as the icon. 46 | /// Override this if the icon and alert classes are different 47 | /// (often they are not). 48 | /// 49 | [HtmlAttributeName("alert-class")] 50 | public string alertClass { get; set; } 51 | 52 | /// 53 | /// If true embeds the message text as HTML. Use this 54 | /// flag if you need to display HTML text. If false 55 | /// the text is HtmlEncoded. 56 | /// 57 | [HtmlAttributeName("message-as-html")] 58 | public bool messageAsHtml { get; set; } 59 | 60 | /// 61 | /// If true embeds the message text as HTML. Use this 62 | /// flag if you need to display HTML text. If false 63 | /// the text is HtmlEncoded. 64 | /// 65 | [HtmlAttributeName("header-as-html")] 66 | public bool headerAsHtml { get; set; } 67 | 68 | public override void Process(TagHelperContext context, TagHelperOutput output) 69 | { 70 | 71 | 72 | if (string.IsNullOrEmpty(message) && string.IsNullOrEmpty(header)) 73 | return; 74 | 75 | if (string.IsNullOrEmpty(icon)) 76 | icon = "warning"; 77 | if (icon == "warning" || icon == "error" || icon == "danger") 78 | icon = icon + " error"; 79 | 80 | if (string.IsNullOrEmpty(alertClass)) 81 | alertClass = icon; 82 | 83 | string messageText = !messageAsHtml ? System.Net.WebUtility.HtmlEncode(message) : message; 84 | 85 | output.TagName = "div"; 86 | 87 | // fix up CSS class 88 | if (cssClass != null) 89 | cssClass = cssClass + " alert alert-" + alertClass; 90 | else 91 | cssClass = "alert alert-" + alertClass; 92 | output.Attributes.Add("class", cssClass); 93 | 94 | if (string.IsNullOrEmpty(header)) 95 | output.Content.SetContent($" {messageText}"); 96 | else 97 | { 98 | output.Content.SetContent( 99 | $"

{header}

\r\n" + 100 | "
\r\n" + 101 | $"{messageText}\r\n"); 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace AlbumViewerNetCore 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | BuildWebHost(args).Run(); 18 | } 19 | 20 | public static IWebHost BuildWebHost(string[] args) 21 | { 22 | return WebHost.CreateDefaultBuilder(args) 23 | .UseStartup() 24 | .Build(); 25 | } 26 | 27 | 28 | 29 | 30 | 31 | //public static void Main(string[] args) 32 | // { 33 | // // use this to allow command line parameters in the config 34 | // var configuration = new ConfigurationBuilder() 35 | // .AddCommandLine(args) 36 | // .Build(); 37 | 38 | 39 | // var hostUrl = configuration["hosturl"]; 40 | // if (string.IsNullOrEmpty(hostUrl)) 41 | // hostUrl = "http://0.0.0.0:5000"; 42 | 43 | 44 | 45 | // var host = new WebHostBuilder() 46 | // .UseConfiguration(configuration) 47 | // //.UseUrls(hostUrl) 48 | // .UseContentRoot(Directory.GetCurrentDirectory()) 49 | // .UseKestrel() 50 | // .UseIISIntegration() 51 | // .UseStartup() 52 | // .Build(); 53 | 54 | // host.Run(); 55 | // } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:26448/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "AlbumViewerNetCore": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | }, 26 | "web": { 27 | "commandName": "web", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Views/AlbumViewerMvc/Album.cshtml: -------------------------------------------------------------------------------- 1 | @model AlbumViewerBusiness.Album 2 |
3 | 4 |
5 | List 6 | Edit 7 | Buy 8 | 9 |
10 | 11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 |

19 | @Model.Title 20 |

21 |
22 | @Model.Title 23 |
24 | 25 |
26 | by @Model.Artist.ArtistName 27 | @( Model.Year > 0 ? "in " + Model.Year : "" ) 28 |
29 |
30 | @Model.Description 31 |
32 | 33 |
34 | 35 | 36 | @foreach (var song in @Model.Tracks) 37 | { 38 | 39 | 40 | 41 | 42 | } 43 |
@song.SongName@song.Length
44 | 45 |
46 | More from 47 | 48 | @Model.Artist.ArtistName
49 | 50 |
51 |
52 |
53 |
-------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Views/AlbumViewerMvc/Albums.cshtml: -------------------------------------------------------------------------------- 1 | @model IEnumerable 2 | 3 |
4 | 5 |
Albums @Model.Count()
6 |
7 | @foreach (var album in Model.ToList()) 8 | { 9 |
10 |
11 |   12 | 13 |
14 |
15 | 16 |
17 |
@album.Title
18 |
19 | @if (album.Artist != null) 20 | { by @album.Artist.ArtistName } 21 | @if (album.Year > 0) 22 | { in @album.Year}
23 |
@album.Description
24 |
25 |
26 |
27 | } 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Views/AlbumViewerMvc/Artists.cshtml: -------------------------------------------------------------------------------- 1 | @model List 2 | 3 | @{ 4 | ViewBag.Title = "title"; 5 | Layout = "_Layout"; 6 | } 7 |
8 | 9 | 10 |
11 | 12 | Artists 13 | @Model.Count 14 | 15 | 16 | @foreach (var artist in Model) 17 | { 18 | 20 |   21 | @artist.AlbumCount 22 | @artist.ArtistName 23 | 24 | } 25 |
26 |
27 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Views/AlbumViewerMvc/TagHelpers.cshtml: -------------------------------------------------------------------------------- 1 | @addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers" 2 | @addTagHelper "*,AlbumViewerAspNet5" 3 | 4 | @model AlbumViewerBusiness.Album 5 | @* 6 | For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 7 | *@ 8 | @{ 9 | ViewBag.Title = "TagHelper Sample"; 10 | } 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |

Test

20 | Albums 22 | 23 | 24 |
25 | 26 | 27 |
28 | 29 |
30 | 31 | 32 |
33 | 34 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Views/Home/About.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "About"; 3 | } 4 |

@ViewBag.Title.

5 |

@ViewBag.Message

6 | 7 |

Use this area to provide additional information.

8 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Views/Home/Contact.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Contact"; 3 | } 4 |

@ViewBag.Title.

5 |

@ViewBag.Message

6 | 7 |
8 | One Microsoft Way
9 | Redmond, WA 98052-6399
10 | P: 11 | 425.555.0100 12 |
13 | 14 |
15 | Support: Support@example.com
16 | Marketing: Marketing@example.com 17 |
18 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Views/Home/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Error"; 3 | 4 | } 5 |

An error occurred

6 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Home Page"; 3 | } 4 | 5 | 67 | 68 |
69 |
70 |

This application consists of:

71 |
    72 |
  • Sample pages showing basic nav
  • 73 |
  • Theming using Bootstrap
  • 74 |
75 |
76 | 89 | 98 |
99 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Error"; 3 | } 4 | 5 |

Error.

6 |

An error occurred while processing your request.

7 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Views/_GlobalImport.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Threading.Tasks 2 | @using AlbumViewerAspNet5 3 | @using AlbumViewerBusiness 4 | @addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers" 5 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using AlbumViewerNetCore 2 | @using AlbumViewerAspNet5 3 | @using AlbumViewerBusiness 4 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 5 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/_Code/ErrorHandling/ApiError.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.ModelBinding; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Westwind.Utilities; 6 | 7 | namespace AlbumViewerAspNetCore 8 | { 9 | 10 | public class ApiError 11 | { 12 | public string message { get; set; } 13 | public bool isError { get; set; } 14 | public string detail { get; set; } 15 | public ValidationErrorCollection errors { get; set; } 16 | 17 | public ApiError(string message) 18 | { 19 | this.message = message; 20 | isError = true; 21 | } 22 | 23 | public ApiError(ModelStateDictionary modelState) 24 | { 25 | this.isError = true; 26 | if (modelState != null && modelState.Any(m => m.Value.Errors.Count > 0)) 27 | { 28 | message = "Please correct the specified errors and try again."; 29 | //errors = modelState.SelectMany(m => m.Value.Errors).ToDictionary(m => m.Key, m=> m.ErrorMessage); 30 | //errors = modelState.SelectMany(m => m.Value.Errors.Select( me => new KeyValuePair( m.Key,me.ErrorMessage) )); 31 | //errors = modelState.SelectMany(m => m.Value.Errors.Select(me => new ModelError { FieldName = m.Key, ErrorMessage = me.ErrorMessage })); 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/_Code/ErrorHandling/ApiException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Westwind.Utilities; 5 | 6 | namespace AlbumViewerAspNetCore 7 | { 8 | 9 | public class ApiException : Exception 10 | { 11 | public int StatusCode { get; set; } 12 | 13 | public ValidationErrorCollection Errors { get; set; } 14 | 15 | public ApiException(string message, 16 | int statusCode = 500, 17 | ValidationErrorCollection errors = null) : 18 | base(message) 19 | { 20 | StatusCode = statusCode; 21 | Errors = errors; 22 | } 23 | public ApiException(Exception ex, int statusCode = 500) : base(ex.Message) 24 | { 25 | StatusCode = statusCode; 26 | } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/_Code/ErrorHandling/ApiExceptionFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc.Filters; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.Logging; 8 | using Serilog; 9 | using Westwind.Utilities; 10 | 11 | namespace AlbumViewerAspNetCore 12 | { 13 | public class ApiExceptionFilter : ExceptionFilterAttribute 14 | { 15 | private ILogger _Logger; 16 | 17 | public ApiExceptionFilter(ILogger logger) 18 | { 19 | _Logger = logger; 20 | } 21 | 22 | 23 | public override void OnException(ExceptionContext context) 24 | { 25 | ApiError apiError = null; 26 | if (context.Exception is ApiException) 27 | { 28 | // handle explicit 'known' API errors 29 | var ex = context.Exception as ApiException; 30 | context.Exception = null; 31 | apiError = new ApiError(ex.Message); 32 | apiError.errors = ex.Errors; 33 | 34 | context.HttpContext.Response.StatusCode = ex.StatusCode; 35 | 36 | _Logger.LogWarning($"Application thrown error: {ex.Message}", ex); 37 | } 38 | else if (context.Exception is UnauthorizedAccessException) 39 | { 40 | apiError = new ApiError("Unauthorized Access"); 41 | context.HttpContext.Response.StatusCode = 401; 42 | _Logger.LogWarning("Unauthorized Access in Controller Filter."); 43 | } 44 | else 45 | { 46 | // Unhandled errors 47 | #if !DEBUG 48 | var msg = "An unhandled error occurred."; 49 | string stack = null; 50 | #else 51 | var msg = context.Exception.GetBaseException().Message; 52 | string stack = context.Exception.StackTrace; 53 | #endif 54 | 55 | apiError = new ApiError(msg); 56 | apiError.detail = stack; 57 | 58 | context.HttpContext.Response.StatusCode = 500; 59 | 60 | // handle logging here 61 | _Logger.LogError(new EventId(0), context.Exception, msg); 62 | } 63 | 64 | // always return a JSON result 65 | context.Result = new JsonResult(apiError); 66 | 67 | base.OnException(context); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Data": { 3 | "Provider": "MySQL", 4 | "ConnectionString": "Server=172.18.245.137;Port=4000;Database=albumviewer;User=root;SslMode=None" 5 | }, 6 | "Logging": { 7 | "IncludeScopes": false, 8 | "LogLevel": { 9 | "Default": "Information", 10 | "System": "Warning", 11 | "Microsoft": "Warning", 12 | "Trace": "Information" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/bundleconfig.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "outputFileName": "wwwroot/css/albumviewer.css", 4 | "inputFiles": [ 5 | "wwwroot/css/albumviewer.css" 6 | ] 7 | } 8 | ] -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/runtimeconfig.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "System.GC.Server": true 3 | } -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/3rdpartylicenses.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/3rdpartylicenses.txt -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/fontawesome-webfont.674f50d287a8c48dc19b.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/fontawesome-webfont.674f50d287a8c48dc19b.eot -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/fontawesome-webfont.af7ae505a9eed503f8b8.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/fontawesome-webfont.af7ae505a9eed503f8b8.woff2 -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/fontawesome-webfont.b06871f281fee6b241d6.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/fontawesome-webfont.b06871f281fee6b241d6.ttf -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/fontawesome-webfont.fee66e712a8a08eef580.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/fontawesome-webfont.fee66e712a8a08eef580.woff -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/glyphicons-halflings-regular.448c34a56d699c29117a.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/glyphicons-halflings-regular.448c34a56d699c29117a.woff2 -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/glyphicons-halflings-regular.e18bbf611f2a2e43afc0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/glyphicons-halflings-regular.e18bbf611f2a2e43afc0.ttf -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/glyphicons-halflings-regular.f4769f9bdb7466be6508.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/glyphicons-halflings-regular.f4769f9bdb7466be6508.eot -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/glyphicons-halflings-regular.fa2772327f55d8198301.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/glyphicons-halflings-regular.fa2772327f55d8198301.woff -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/images/RockLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/images/RockLogo.png -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/images/artists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/images/artists.png -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/images/artists32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/images/artists32.png -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/images/gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/images/gear.png -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/images/headphone-head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/images/headphone-head.png -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/images/icon.png -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/images/record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/images/record.png -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/images/search.png -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/images/search_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/images/search_box.png -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/images/search_box2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/images/search_box2.png -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/images/webconnectionicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/images/webconnectionicon.png -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/images/webconnectionlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/images/webconnectionlogo.png -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/images/westwindtext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockersamples/dotnet-album-viewer/2c8df52588566d9eadf1e637c47a41eb8ecc5220/src/AlbumViewerNetCore/wwwroot/images/westwindtext.png -------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | West Wind Album Viewer

Getting things ready...

-------------------------------------------------------------------------------- /src/AlbumViewerNetCore/wwwroot/inline.6ab1992b1d6b43743130.bundle.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}var n=window.webpackJsonp;window.webpackJsonp=function(t,c,a){for(var u,i,f,l=0,s=[];l 2 | 3 | netstandard2.0 4 | 5 | 6 | --------------------------------------------------------------------------------