diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c602e72..b518125 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v3 - uses: subosito/flutter-action@v2 with: - flutter-version: '3.10.0' + flutter-version: '3.27.1' channel: 'stable' - run: flutter pub get - run: flutter pub run build_runner build --delete-conflicting-outputs diff --git a/curl_output.json b/curl_output.json new file mode 100644 index 0000000..efb1a11 --- /dev/null +++ b/curl_output.json @@ -0,0 +1 @@ +{"data":[{"_id":"60127_160612","id":"60127_160612","number":"8818N","team_name":"Ctrl X","organization":"Roboplanet","location":{"venue":null,"address_1":"","address_2":null,"city":"Richmond","region":"British Columbia","postcode":"V6X 1X5","country":"Canada","coordinates":{"lat":49.2,"lon":-123.1}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":33.16,"teamworkInfo":{"teamwork":33.16,"teamworkSigma":2.76,"teamworkQuality":24.87,"epa":201.42,"consistency":0.185,"matchCount":43},"consistency":0.185,"epa":201.42,"skills":{"type":"skills_derived","mu":49.61,"sigma":8.33,"score":49.61,"percentile":99.23},"performance":13.62,"performanceInfo":{"mu":21.53,"sigma":2.64,"score":13.62}}},{"_id":"60127_169947","id":"60127_169947","number":"9933X","team_name":"Cybot-PoleXtar(CPA)","organization":"Cybot Robotics Academy.","location":{"venue":null,"address_1":"","address_2":null,"city":"Vancouver","region":"British Columbia","postcode":"V6S 1H3","country":"Canada","coordinates":{"lat":49.3,"lon":-123.2}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":31.04,"teamworkInfo":{"teamwork":31.04,"teamworkSigma":2.38,"teamworkQuality":23.89,"epa":170.81,"consistency":0.302,"matchCount":54},"consistency":0.302,"epa":170.81,"skills":{"type":"skills_derived","mu":49.15,"sigma":8.33,"score":49.15,"percentile":98.3},"performance":10.97,"performanceInfo":{"mu":17.78,"sigma":2.27,"score":10.97}}},{"_id":"60062_172314","id":"60062_172314","number":"6677N","team_name":"ReSOLution","organization":"Dr. Player Robotic Lab . Nantou","location":{"venue":null,"address_1":"","address_2":null,"city":"Nantou","region":null,"postcode":"542","country":"Chinese Taipei","coordinates":{"lat":24,"lon":120.7}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":28.07,"teamworkInfo":{"teamwork":28.07,"teamworkSigma":3.47,"teamworkQuality":17.66,"epa":167.69,"consistency":0.244,"matchCount":26},"consistency":0.244,"epa":167.69,"skills":{"type":"skills_derived","mu":49.55,"sigma":8.33,"score":49.55,"percentile":99.1},"performance":9.74,"performanceInfo":{"mu":19.07,"sigma":3.11,"score":9.74}}},{"_id":"60127_107011","id":"60127_107011","number":"3150X","team_name":"Vex-Wings","organization":"Independent Team","location":{"venue":null,"address_1":"","address_2":null,"city":"Cumming","region":"Georgia","postcode":"30041","country":"United States","coordinates":{"lat":34.1,"lon":-84.2}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":25.11,"teamworkInfo":{"teamwork":25.11,"teamworkSigma":1.41,"teamworkQuality":20.87,"epa":29.85,"consistency":0.65,"matchCount":127},"consistency":0.65,"epa":29.85,"skills":{"type":"skills_derived","mu":46.95,"sigma":8.33,"score":46.95,"percentile":93.91},"performance":7.44,"performanceInfo":{"mu":14.08,"sigma":2.21,"score":7.44}}},{"_id":"60127_170064","id":"60127_170064","number":"286B","team_name":"Blaze","organization":"Magikid Tustin","location":{"venue":null,"address_1":"","address_2":null,"city":"Tustin","region":"California","postcode":"92780","country":"United States","coordinates":{"lat":33.8,"lon":-117.8}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":24.07,"teamworkInfo":{"teamwork":24.07,"teamworkSigma":2.2,"teamworkQuality":17.46,"epa":171.01,"consistency":0.326,"matchCount":52},"consistency":0.326,"epa":171.01,"skills":{"type":"skills_derived","mu":49.73,"sigma":8.33,"score":49.73,"percentile":99.47},"performance":9.79,"performanceInfo":{"mu":16.66,"sigma":2.29,"score":9.79}}},{"_id":"60062_115890","id":"60062_115890","number":"62153A","team_name":"CMS1","organization":"Concordia Middle & High School","location":{"venue":null,"address_1":"","address_2":null,"city":"Chiayi County","region":null,"postcode":"621015","country":"Chinese Taipei","coordinates":{"lat":23.5,"lon":120.4}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":22.69,"teamworkInfo":{"teamwork":22.69,"teamworkSigma":3.67,"teamworkQuality":11.69,"epa":135.82,"consistency":0.34,"matchCount":18},"consistency":0.34,"epa":135.82,"skills":{"type":"skills_derived","mu":48.83,"sigma":8.33,"score":48.83,"percentile":97.65},"performance":7.69,"performanceInfo":{"mu":19.41,"sigma":3.91,"score":7.69}}},{"_id":"60062_170392","id":"60062_170392","number":"8676A","team_name":"8676A","organization":"CHINGSHIN ACADEMY","location":{"venue":null,"address_1":"","address_2":null,"city":"Taipei","region":null,"postcode":"116","country":"Chinese Taipei","coordinates":{"lat":25,"lon":121.5}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"consistency":0.74,"epa":67.03,"teamwork":22.41,"teamworkInfo":{"teamwork":22.41,"teamworkSigma":1.89,"teamworkQuality":16.73,"epa":67.03,"consistency":0.74,"matchCount":64},"skills":{"type":"skills_derived","mu":45.83,"sigma":8.33,"score":45.83,"percentile":91.66},"performance":7.17,"performanceInfo":{"mu":16.67,"sigma":3.17,"score":7.17}}},{"_id":"60127_189960","id":"60127_189960","number":"81777T","team_name":"Captain IQ","organization":"Magikid Cupertino Robotics Lab","location":{"venue":null,"address_1":"","address_2":null,"city":"Cupertino","region":"California","postcode":"95014","country":"United States","coordinates":{"lat":37.3,"lon":-122}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":22.23,"teamworkInfo":{"teamwork":22.23,"teamworkSigma":2.16,"teamworkQuality":15.75,"epa":172.64,"consistency":0.458,"matchCount":52},"consistency":0.458,"epa":172.64,"skills":{"type":"skills_derived","mu":49.93,"sigma":8.33,"score":49.93,"percentile":99.86},"performance":10.68,"performanceInfo":{"mu":17.27,"sigma":2.2,"score":10.68}}},{"_id":"60062_174713","id":"60062_174713","number":"6611V","team_name":"Universe","organization":"Dr.player robotics lab Zhubei","location":{"venue":null,"address_1":"","address_2":null,"city":"Hsinchu County","region":null,"postcode":"302","country":"Chinese Taipei","coordinates":{"lat":24.8,"lon":121}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":21.53,"teamworkInfo":{"teamwork":21.53,"teamworkSigma":3.04,"teamworkQuality":12.42,"epa":142.66,"consistency":0.296,"matchCount":26},"consistency":0.296,"epa":142.66,"skills":{"type":"skills_derived","mu":48.42,"sigma":8.33,"score":48.42,"percentile":96.85},"performance":8.51,"performanceInfo":{"mu":17.74,"sigma":3.08,"score":8.51}}},{"_id":"60062_175103","id":"60062_175103","number":"5491M","team_name":"HAPPY HIPPO","organization":"Fancy Robotics International","location":{"venue":null,"address_1":"","address_2":null,"city":"Taipei City","region":null,"postcode":"111081","country":"Chinese Taipei","coordinates":{"lat":25.1,"lon":121.5}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"consistency":0.29,"epa":137.34,"teamwork":21.26,"teamworkInfo":{"teamwork":21.26,"teamworkSigma":2.48,"teamworkQuality":13.82,"epa":137.34,"consistency":0.29,"matchCount":44},"skills":{"type":"skills_derived","mu":49.97,"sigma":8.33,"score":49.97,"percentile":99.94},"performance":5.83,"performanceInfo":{"mu":12.97,"sigma":2.38,"score":5.83}}},{"_id":"60224_184835","id":"60224_184835","number":"93601A","team_name":"Bazza","organization":"Shore","location":{"venue":null,"address_1":"","address_2":null,"city":"North Sydney","region":"New South Wales","postcode":"2060","country":"Australia","coordinates":{"lat":-33.8,"lon":151.2}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":21.06,"teamworkInfo":{"teamwork":21.06,"teamworkSigma":1.42,"teamworkQuality":16.81,"epa":68.69,"consistency":0.8,"matchCount":117},"consistency":0.8,"epa":68.69,"skills":{"type":"skills_derived","mu":30.52,"sigma":8.33,"score":30.52,"percentile":61.05},"performance":1.81,"performanceInfo":{"mu":8.05,"sigma":2.08,"score":1.81}}},{"_id":"60127_87113","id":"60127_87113","number":"74801A","team_name":"Green Chili Motǝr watermelon","organization":"WESTWOOD MIDDLE SCHOOL","location":{"venue":null,"address_1":"","address_2":null,"city":"Gainesville","region":"Florida","postcode":"32605","country":"United States","coordinates":{"lat":29.7,"lon":-82.4}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":21.01,"teamworkInfo":{"teamwork":21.01,"teamworkSigma":1.79,"teamworkQuality":15.63,"epa":32.6,"consistency":0.494,"matchCount":72},"consistency":0.494,"epa":32.6,"skills":{"type":"skills_derived","mu":25.31,"sigma":8.33,"score":25.31,"percentile":50.61},"performance":-4.06,"performanceInfo":{"mu":3.75,"sigma":2.6,"score":-4.06}}},{"_id":"60062_156628","id":"60062_156628","number":"67899A","team_name":"Crazy Raccoon","organization":"PU TAI JUNIOR HIGH SCHOOL","location":{"venue":null,"address_1":"","address_2":null,"city":"Nantou","region":null,"postcode":"545","country":"Chinese Taipei","coordinates":{"lat":24,"lon":120.9}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":20.84,"teamworkInfo":{"teamwork":20.84,"teamworkSigma":2.91,"teamworkQuality":12.12,"epa":123.05,"consistency":0.294,"matchCount":26},"consistency":0.294,"epa":123.05,"skills":{"type":"skills_derived","mu":48.03,"sigma":8.33,"score":48.03,"percentile":96.06},"performance":8.7,"performanceInfo":{"mu":18.7,"sigma":3.34,"score":8.7}}},{"_id":"60062_156630","id":"60062_156630","number":"6699G","team_name":"OP Turtle","organization":"Taiwan Robotics Lab","location":{"venue":null,"address_1":"","address_2":null,"city":"Taichung","region":null,"postcode":"408340","country":"Chinese Taipei","coordinates":{"lat":24.2,"lon":120.7}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":20.52,"teamworkInfo":{"teamwork":20.52,"teamworkSigma":2.96,"teamworkQuality":11.64,"epa":138.94,"consistency":0.258,"matchCount":26},"consistency":0.258,"epa":138.94,"skills":{"type":"skills_derived","mu":49.69,"sigma":8.33,"score":49.69,"percentile":99.37},"performance":11.09,"performanceInfo":{"mu":21.58,"sigma":3.5,"score":11.09}}},{"_id":"60106_193049","id":"60106_193049","number":"13722Z","team_name":"Lightning Panther","organization":"The FIG","location":{"venue":null,"address_1":"","address_2":null,"city":"Gainesville","region":"Florida","postcode":"32606","country":"United States","coordinates":{"lat":29.7,"lon":-82.4}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":20.33,"teamworkInfo":{"teamwork":20.33,"teamworkSigma":2.13,"teamworkQuality":13.94,"epa":16.42,"consistency":0.794,"matchCount":45},"consistency":0.794,"epa":16.42,"skills":{"type":"skills_derived","mu":38.34,"sigma":8.33,"score":38.34,"percentile":76.67},"performance":0.14,"performanceInfo":{"mu":11.06,"sigma":3.64,"score":0.14}}},{"_id":"60062_141898","id":"60062_141898","number":"6699C","team_name":"Electric Bear","organization":"Taiwan Robotics Lab","location":{"venue":null,"address_1":"","address_2":null,"city":"Taichung","region":null,"postcode":"408340","country":"Chinese Taipei","coordinates":{"lat":24.2,"lon":120.7}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":20.22,"teamworkInfo":{"teamwork":20.22,"teamworkSigma":2.97,"teamworkQuality":11.32,"epa":122.61,"consistency":0.393,"matchCount":26},"consistency":0.393,"epa":122.61,"skills":{"type":"skills_derived","mu":49.13,"sigma":8.33,"score":49.13,"percentile":98.26},"performance":7.27,"performanceInfo":{"mu":16.15,"sigma":2.96,"score":7.27}}},{"_id":"60127_141481","id":"60127_141481","number":"252W","team_name":"Wonder Why…?","organization":"I Wonder Robotics","location":{"venue":null,"address_1":"","address_2":null,"city":"Richmond Hill","region":"Ontario","postcode":"L4B3R3","country":"Canada","coordinates":{"lat":43.9,"lon":-79.4}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":20.21,"teamworkInfo":{"teamwork":20.21,"teamworkSigma":1.79,"teamworkQuality":14.83,"epa":143.33,"consistency":0.416,"matchCount":78},"consistency":0.416,"epa":143.33,"skills":{"type":"skills_derived","mu":49.23,"sigma":8.33,"score":49.23,"percentile":98.46},"performance":6.68,"performanceInfo":{"mu":11.92,"sigma":1.75,"score":6.68}}},{"_id":"60140_126531","id":"60140_126531","number":"13205A","team_name":"TCCS A Team","organization":"Tung Chung Catholic School(Secondary Section)","location":{"venue":null,"address_1":"","address_2":null,"city":"Islands District","region":null,"postcode":"000000","country":"Hong Kong, China","coordinates":{"lat":22.3,"lon":113.9}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":20.12,"teamworkInfo":{"teamwork":20.12,"teamworkSigma":1.65,"teamworkQuality":15.17,"epa":83.62,"consistency":0.574,"matchCount":88},"consistency":0.574,"epa":83.62,"skills":{"type":"skills_derived","mu":44.77,"sigma":8.33,"score":44.77,"percentile":89.55},"performance":10.65,"performanceInfo":{"mu":18.83,"sigma":2.73,"score":10.65}}},{"_id":"60127_189864","id":"60127_189864","number":"81777Z","team_name":"Hot Slice","organization":"Magikid Cupertino Robotics Lab","location":{"venue":null,"address_1":"","address_2":null,"city":"Cupertino","region":"California","postcode":"95014","country":"United States","coordinates":{"lat":37.3,"lon":-122}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":20.06,"teamworkInfo":{"teamwork":20.06,"teamworkSigma":1.91,"teamworkQuality":14.33,"epa":191.52,"consistency":0.474,"matchCount":66},"consistency":0.474,"epa":191.52,"skills":{"type":"skills_derived","mu":49.99,"sigma":8.33,"score":49.99,"percentile":99.98},"performance":10.76,"performanceInfo":{"mu":16.6,"sigma":1.95,"score":10.76}}},{"_id":"60127_184555","id":"60127_184555","number":"286C","team_name":"Hotpocket","organization":"Magikid Tustin","location":{"venue":null,"address_1":"","address_2":null,"city":"Tustin","region":"California","postcode":"92780","country":"United States","coordinates":{"lat":33.8,"lon":-117.8}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":19.81,"teamworkInfo":{"teamwork":19.81,"teamworkSigma":2.09,"teamworkQuality":13.54,"epa":179.28,"consistency":0.434,"matchCount":55},"consistency":0.434,"epa":179.28,"skills":{"type":"skills_derived","mu":49.78,"sigma":8.33,"score":49.78,"percentile":99.57},"performance":10.72,"performanceInfo":{"mu":17.15,"sigma":2.14,"score":10.72}}},{"_id":"60026_180686","id":"60026_180686","number":"87660A","team_name":"The Rotten Omlettes","organization":"FS Education Brooklyn","location":{"venue":null,"address_1":"","address_2":null,"city":"Brooklyn","region":"New York","postcode":"11220","country":"United States","coordinates":{"lat":40.6,"lon":-74}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":19.78,"teamworkInfo":{"teamwork":19.78,"teamworkSigma":2.96,"teamworkQuality":10.88,"epa":150.8,"consistency":0.265,"matchCount":26},"consistency":0.265,"epa":150.8,"skills":{"type":"skills_derived","mu":48.2,"sigma":8.33,"score":48.2,"percentile":96.4},"performance":6.88,"performanceInfo":{"mu":16.36,"sigma":3.16,"score":6.88}}},{"_id":"60127_173452","id":"60127_173452","number":"95014X","team_name":"GaGaGuGu","organization":"STEM Cupertino","location":{"venue":null,"address_1":"","address_2":null,"city":"Cupertino","region":"California","postcode":"95014","country":"United States","coordinates":{"lat":37.3,"lon":-122}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":19.54,"teamworkInfo":{"teamwork":19.54,"teamworkSigma":1.43,"teamworkQuality":15.24,"epa":155.57,"consistency":0.416,"matchCount":132},"consistency":0.416,"epa":155.57,"skills":{"type":"skills_derived","mu":49.9,"sigma":8.33,"score":49.9,"percentile":99.81},"performance":8.8,"performanceInfo":{"mu":13.04,"sigma":1.42,"score":8.8}}},{"_id":"60062_142933","id":"60062_142933","number":"66799Y","team_name":"AST-onishing Y","organization":"American School in Taichung","location":{"venue":null,"address_1":"","address_2":null,"city":"Taichung","region":null,"postcode":"40661","country":"Chinese Taipei","coordinates":{"lat":46.3,"lon":-63.1}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":19.3,"teamworkInfo":{"teamwork":19.3,"teamworkSigma":3.08,"teamworkQuality":10.05,"epa":134.29,"consistency":0.355,"matchCount":26},"consistency":0.355,"epa":134.29,"skills":{"type":"skills_derived","mu":49.73,"sigma":8.33,"score":49.73,"percentile":99.45},"performance":3.91,"performanceInfo":{"mu":12.97,"sigma":3.02,"score":3.91}}},{"_id":"60258_172166","id":"60258_172166","number":"25368A","team_name":"Bruin Bots","organization":"WESTERN BRANCH MIDDLE","location":{"venue":null,"address_1":"","address_2":null,"city":"Chesapeake","region":"Virginia","postcode":"23321","country":"United States","coordinates":{"lat":36.9,"lon":-76.4}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":18.45,"teamworkInfo":{"teamwork":18.45,"teamworkSigma":3.58,"teamworkQuality":7.71,"epa":20.64,"consistency":0.545,"matchCount":18},"consistency":0.545,"epa":20.64,"skills":{"type":"skills_derived","mu":13.84,"sigma":8.33,"score":13.84,"percentile":27.68},"performance":0.83,"performanceInfo":{"mu":19.58,"sigma":6.25,"score":0.83}}},{"_id":"60303_154252","id":"60303_154252","number":"48495C","team_name":"Cats Wearing Caps","organization":"Rovers Robotics","location":{"venue":null,"address_1":"","address_2":null,"city":"Palmerston North","region":"Manawatu-Wanganui","postcode":"4410","country":"New Zealand","coordinates":{"lat":-40.4,"lon":175.6}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":18.34,"teamworkInfo":{"teamwork":18.34,"teamworkSigma":2.51,"teamworkQuality":10.81,"epa":158.51,"consistency":0.514,"matchCount":37},"consistency":0.514,"epa":158.51,"skills":{"type":"skills_derived","mu":46.88,"sigma":8.33,"score":46.88,"percentile":93.76},"performance":9.84,"performanceInfo":{"mu":17.37,"sigma":2.51,"score":9.84}}},{"_id":"60127_156546","id":"60127_156546","number":"8818M","team_name":"Mercury","organization":"Roboplanet","location":{"venue":null,"address_1":"","address_2":null,"city":"Richmond","region":"British Columbia","postcode":"V6X 1X5","country":"Canada","coordinates":{"lat":49.2,"lon":-123.1}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":17.56,"teamworkInfo":{"teamwork":17.56,"teamworkSigma":2.33,"teamworkQuality":10.58,"epa":120.25,"consistency":0.449,"matchCount":46},"consistency":0.449,"epa":120.25,"skills":{"type":"skills_derived","mu":48.22,"sigma":8.33,"score":48.22,"percentile":96.43},"performance":8.11,"performanceInfo":{"mu":15.17,"sigma":2.35,"score":8.11}}},{"_id":"60062_188889","id":"60062_188889","number":"6677U","team_name":"Never Give up","organization":"Dr. Player Robotic Lab . Nantou","location":{"venue":null,"address_1":"","address_2":null,"city":"Nantou","region":null,"postcode":"542","country":"Chinese Taipei","coordinates":{"lat":24,"lon":120.7}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":17.55,"teamworkInfo":{"teamwork":17.55,"teamworkSigma":3.1,"teamworkQuality":8.26,"epa":142.48,"consistency":0.325,"matchCount":27},"consistency":0.325,"epa":142.48,"skills":{"type":"skills_derived","mu":49.95,"sigma":8.33,"score":49.95,"percentile":99.9},"performance":4.03,"performanceInfo":{"mu":13.33,"sigma":3.1,"score":4.03}}},{"_id":"60140_136786","id":"60140_136786","number":"19375B","team_name":"IMSC Team B","organization":"Munsang College (Hong Kong Island)","location":{"venue":null,"address_1":"","address_2":null,"city":"Sai Wan Ho, Hong Kong Island","region":null,"postcode":"000000","country":"Hong Kong, China","coordinates":{"lat":22.3,"lon":114.2}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":17.48,"teamworkInfo":{"teamwork":17.48,"teamworkSigma":3.06,"teamworkQuality":8.3,"epa":152.86,"consistency":0.45,"matchCount":23},"consistency":0.45,"epa":152.86,"skills":{"type":"skills_derived","mu":49.41,"sigma":8.33,"score":49.41,"percentile":98.83},"performance":11.95,"performanceInfo":{"mu":22.46,"sigma":3.5,"score":11.95}}},{"_id":"60127_174108","id":"60127_174108","number":"3722U","team_name":"Quasar","organization":"The Frazer School","location":{"venue":null,"address_1":"","address_2":null,"city":"Gainesville","region":"Florida","postcode":"32606","country":"United States","coordinates":{"lat":29.7,"lon":-82.4}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":17.3,"teamworkInfo":{"teamwork":17.3,"teamworkSigma":2.86,"teamworkQuality":8.74,"epa":133.35,"consistency":0.477,"matchCount":26},"consistency":0.477,"epa":133.35,"skills":{"type":"skills_derived","mu":48.43,"sigma":8.33,"score":48.43,"percentile":96.86},"performance":8.67,"performanceInfo":{"mu":17.94,"sigma":3.09,"score":8.67}}},{"_id":"60127_177783","id":"60127_177783","number":"9933Y","team_name":"Cybot-Lonosphere","organization":"Cybot Robotics Academy","location":{"venue":null,"address_1":"","address_2":null,"city":"Vancouver","region":"British Columbia","postcode":"V6S 1H3","country":"Canada","coordinates":{"lat":49.3,"lon":-123.2}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":17.01,"teamworkInfo":{"teamwork":17.01,"teamworkSigma":1.95,"teamworkQuality":11.15,"epa":145.88,"consistency":0.314,"matchCount":63},"consistency":0.314,"epa":145.88,"skills":{"type":"skills_derived","mu":48.63,"sigma":8.33,"score":48.63,"percentile":97.27},"performance":7.54,"performanceInfo":{"mu":13.48,"sigma":1.98,"score":7.54}}},{"_id":"60062_142931","id":"60062_142931","number":"66799C","team_name":"AST-onishing C","organization":"American School in Taichung","location":{"venue":null,"address_1":"","address_2":null,"city":"Taichung","region":null,"postcode":"40661","country":"Chinese Taipei","coordinates":{"lat":46.3,"lon":-63.1}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":16.99,"teamworkInfo":{"teamwork":16.99,"teamworkSigma":3.48,"teamworkQuality":6.56,"epa":116.41,"consistency":0.312,"matchCount":18},"consistency":0.312,"epa":116.41,"skills":{"type":"skills_derived","mu":48.6,"sigma":8.33,"score":48.6,"percentile":97.2},"performance":9.43,"performanceInfo":{"mu":21.65,"sigma":4.07,"score":9.43}}},{"_id":"60127_172882","id":"60127_172882","number":"10700Z","team_name":"Fluffy Bunny","organization":"Great Minds Robotics","location":{"venue":null,"address_1":"","address_2":null,"city":"Los Angeles","region":"California","postcode":"91367","country":"United States","coordinates":{"lat":34.2,"lon":-118.6}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":16.88,"teamworkInfo":{"teamwork":16.88,"teamworkSigma":2.79,"teamworkQuality":8.52,"epa":149.19,"consistency":0.424,"matchCount":31},"consistency":0.424,"epa":149.19,"skills":{"type":"skills_derived","mu":48.68,"sigma":8.33,"score":48.68,"percentile":97.36},"performance":11.64,"performanceInfo":{"mu":20.77,"sigma":3.04,"score":11.64}}},{"_id":"60140_103059","id":"60140_103059","number":"4815A","team_name":"CPC Robotics A","organization":"WEO Chang Pui Chung Memorial School","location":{"venue":null,"address_1":"","address_2":null,"city":"New Territories","region":null,"postcode":"00000","country":"Hong Kong, China","coordinates":{"lat":22.3,"lon":114.3}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":16.81,"teamworkInfo":{"teamwork":16.81,"teamworkSigma":1.61,"teamworkQuality":11.99,"epa":112.57,"consistency":0.671,"matchCount":102},"consistency":0.671,"epa":112.57,"skills":{"type":"skills_derived","mu":48.64,"sigma":8.33,"score":48.64,"percentile":97.28},"performance":9.11,"performanceInfo":{"mu":15.42,"sigma":2.1,"score":9.11}}},{"_id":"60062_110831","id":"60062_110831","number":"6699W","team_name":"Velocity Vortex","organization":"Taiwan Robotics Lab","location":{"venue":null,"address_1":"","address_2":null,"city":"Taichung","region":null,"postcode":"408340","country":"Chinese Taipei","coordinates":{"lat":24.2,"lon":120.7}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":16.32,"teamworkInfo":{"teamwork":16.32,"teamworkSigma":2.95,"teamworkQuality":7.46,"epa":137.68,"consistency":0.349,"matchCount":26},"consistency":0.349,"epa":137.68,"skills":{"type":"skills_derived","mu":49.98,"sigma":8.33,"score":49.98,"percentile":99.95},"performance":5.99,"performanceInfo":{"mu":15.3,"sigma":3.1,"score":5.99}}},{"_id":"60062_171668","id":"60062_171668","number":"88771C","team_name":"XiYuan Go Go","organization":"臺中市立西苑高級中學國中部","location":{"venue":null,"address_1":"","address_2":null,"city":"Taichung","region":null,"postcode":"407","country":"Chinese Taipei","coordinates":{"lat":24.2,"lon":120.6}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":16.32,"teamworkInfo":{"teamwork":16.32,"teamworkSigma":3.54,"teamworkQuality":5.71,"epa":91.63,"consistency":0.411,"matchCount":18},"consistency":0.411,"epa":91.63,"skills":{"type":"skills_derived","mu":41.71,"sigma":8.33,"score":41.71,"percentile":83.42},"performance":4.6,"performanceInfo":{"mu":15.3,"sigma":3.57,"score":4.6}}},{"_id":"60062_189398","id":"60062_189398","number":"8651B","team_name":"DCT Five Little Pigs","organization":"夢想機器人教室","location":{"venue":null,"address_1":"","address_2":null,"city":"New Taipei City","region":null,"postcode":"244015","country":"Chinese Taipei","coordinates":{"lat":25.1,"lon":121.4}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":16.29,"teamworkInfo":{"teamwork":16.29,"teamworkSigma":3.44,"teamworkQuality":5.99,"epa":87.56,"consistency":0.396,"matchCount":18},"consistency":0.396,"epa":87.56,"skills":{"type":"skills_derived","mu":40.04,"sigma":8.33,"score":40.04,"percentile":80.08},"performance":5.19,"performanceInfo":{"mu":16.29,"sigma":3.7,"score":5.19}}},{"_id":"60062_188885","id":"60062_188885","number":"66990Z","team_name":"Elite","organization":"Dr. Player Robotic Lab . Taichung","location":{"venue":null,"address_1":"","address_2":null,"city":"Taichung City","region":null,"postcode":"408","country":"Chinese Taipei","coordinates":{"lat":24.1,"lon":120.6}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":15.73,"teamworkInfo":{"teamwork":15.73,"teamworkSigma":2.91,"teamworkQuality":7,"epa":122.61,"consistency":0.465,"matchCount":26},"consistency":0.465,"epa":122.61,"skills":{"type":"skills_derived","mu":48.47,"sigma":8.33,"score":48.47,"percentile":96.95},"performance":7,"performanceInfo":{"mu":16.04,"sigma":3.01,"score":7}}},{"_id":"60140_149920","id":"60140_149920","number":"55868C","team_name":"55868C","organization":"PUICHING MIDDLE SCHOOL MACAU","location":{"venue":null,"address_1":"","address_2":null,"city":"Macau","region":null,"postcode":"999078","country":"Macau, China","coordinates":{"lat":22.2,"lon":113.5}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":15.56,"teamworkInfo":{"teamwork":15.56,"teamworkSigma":3.56,"teamworkQuality":4.87,"epa":106.57,"consistency":0.353,"matchCount":17},"consistency":0.353,"epa":106.57,"skills":{"type":"skills_derived","mu":44.05,"sigma":8.33,"score":44.05,"percentile":88.1},"performance":7.59,"performanceInfo":{"mu":18.7,"sigma":3.7,"score":7.59}}},{"_id":"60127_156547","id":"60127_156547","number":"8818S","team_name":"The Clashers","organization":"Roboplanet","location":{"venue":null,"address_1":"","address_2":null,"city":"Richmond","region":"British Columbia","postcode":"V6X 1X5","country":"Canada","coordinates":{"lat":49.2,"lon":-123.1}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":15.4,"teamworkInfo":{"teamwork":15.4,"teamworkSigma":2.28,"teamworkQuality":8.56,"epa":144.55,"consistency":0.377,"matchCount":46},"consistency":0.377,"epa":144.55,"skills":{"type":"skills_derived","mu":48.97,"sigma":8.33,"score":48.97,"percentile":97.94},"performance":9.71,"performanceInfo":{"mu":16.63,"sigma":2.31,"score":9.71}}},{"_id":"60062_171899","id":"60062_171899","number":"16899A","team_name":"柴藝不凡","organization":"嘉義柴林創意機器人","location":{"venue":null,"address_1":"","address_2":null,"city":"嘉義縣","region":null,"postcode":"623","country":"Chinese Taipei","coordinates":{"lat":23.6,"lon":120.4}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":15.28,"teamworkInfo":{"teamwork":15.28,"teamworkSigma":3.32,"teamworkQuality":5.34,"epa":103.25,"consistency":0.323,"matchCount":18},"consistency":0.323,"epa":103.25,"skills":{"type":"skills_derived","mu":46.74,"sigma":8.33,"score":46.74,"percentile":93.49},"performance":1.12,"performanceInfo":{"mu":11.6,"sigma":3.49,"score":1.12}}},{"_id":"60300_176547","id":"60300_176547","number":"4250X","team_name":"Bytesize","organization":"MuYv Robotics Consulting","location":{"venue":null,"address_1":"","address_2":null,"city":"Wellington","region":"Wellington","postcode":"6037","country":"New Zealand","coordinates":{"lat":-41.2,"lon":174.8}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":14.88,"teamworkInfo":{"teamwork":14.88,"teamworkSigma":2.58,"teamworkQuality":7.15,"epa":126.05,"consistency":0.523,"matchCount":37},"consistency":0.523,"epa":126.05,"skills":{"type":"skills_derived","mu":46.81,"sigma":8.33,"score":46.81,"percentile":93.63},"performance":6.52,"performanceInfo":{"mu":13.82,"sigma":2.43,"score":6.52}}},{"_id":"60303_176547","id":"60303_176547","number":"4250X","team_name":"Bytesize","organization":"MuYv Robotics Consulting","location":{"venue":null,"address_1":"","address_2":null,"city":"Wellington","region":"Wellington","postcode":"6037","country":"New Zealand","coordinates":{"lat":-41.2,"lon":174.8}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":14.88,"teamworkInfo":{"teamwork":14.88,"teamworkSigma":2.58,"teamworkQuality":7.15,"epa":126.05,"consistency":0.523,"matchCount":37},"consistency":0.523,"epa":126.05,"skills":{"type":"skills_derived","mu":46.81,"sigma":8.33,"score":46.81,"percentile":93.63},"performance":6.52,"performanceInfo":{"mu":13.82,"sigma":2.43,"score":6.52}}},{"_id":"60062_172312","id":"60062_172312","number":"14510Y","team_name":"Fly Eagle","organization":"Mingdao high school","location":{"venue":null,"address_1":"","address_2":null,"city":"Wuri Dist.","region":null,"postcode":"41401","country":"Chinese Taipei","coordinates":{"lat":24.1,"lon":120.6}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":14.83,"teamworkInfo":{"teamwork":14.83,"teamworkSigma":2.89,"teamworkQuality":6.16,"epa":110.09,"consistency":0.315,"matchCount":26},"consistency":0.315,"epa":110.09,"skills":{"type":"skills_derived","mu":49.25,"sigma":8.33,"score":49.25,"percentile":98.5},"performance":4.84,"performanceInfo":{"mu":14.07,"sigma":3.08,"score":4.84}}},{"_id":"60062_187095","id":"60062_187095","number":"70090T","team_name":"O.O","organization":"Fancy Innovation Lab","location":{"venue":null,"address_1":"","address_2":null,"city":"Taipei","region":null,"postcode":"11168","country":"Chinese Taipei","coordinates":{"lat":25.1,"lon":121.5}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"consistency":0.396,"epa":115.93,"teamwork":14.52,"teamworkInfo":{"teamwork":14.52,"teamworkSigma":2.7,"teamworkQuality":6.43,"epa":115.93,"consistency":0.396,"matchCount":34},"skills":{"type":"skills_derived","mu":44.36,"sigma":8.33,"score":44.36,"percentile":88.73},"performance":3.35,"performanceInfo":{"mu":11.52,"sigma":2.72,"score":3.35}}},{"_id":"60127_189486","id":"60127_189486","number":"6177G","team_name":"Ghostly Geckos","organization":"Robotfun Academy","location":{"venue":null,"address_1":"","address_2":null,"city":"Wellesley Hills","region":"Massachusetts","postcode":"02481","country":"United States","coordinates":{"lat":42.3,"lon":-71.3}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":14.46,"teamworkInfo":{"teamwork":14.46,"teamworkSigma":2.29,"teamworkQuality":7.59,"epa":120.49,"consistency":0.396,"matchCount":45},"consistency":0.396,"epa":120.49,"skills":{"type":"skills_derived","mu":49.16,"sigma":8.33,"score":49.16,"percentile":98.31},"performance":7.43,"performanceInfo":{"mu":14.42,"sigma":2.33,"score":7.43}}},{"_id":"60062_188203","id":"60062_188203","number":"5408B","team_name":"小哈機器人","organization":"小哈機器人 HaRobot","location":{"venue":null,"address_1":"","address_2":null,"city":"高雄市 / Kaohsiung City","region":null,"postcode":"807","country":"Chinese Taipei","coordinates":{"lat":22.7,"lon":120.3}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":14.33,"teamworkInfo":{"teamwork":14.33,"teamworkSigma":3.39,"teamworkQuality":4.15,"epa":100.41,"consistency":0.393,"matchCount":18},"consistency":0.393,"epa":100.41,"skills":{"type":"skills_derived","mu":46.96,"sigma":8.33,"score":46.96,"percentile":93.92},"performance":6.11,"performanceInfo":{"mu":17.12,"sigma":3.67,"score":6.11}}},{"_id":"60062_170393","id":"60062_170393","number":"8676B","team_name":"Vextronauts 8676","organization":"CHINGSHIN ACADEMY","location":{"venue":null,"address_1":"","address_2":null,"city":"Taipei","region":null,"postcode":"116","country":"Chinese Taipei","coordinates":{"lat":25,"lon":121.5}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"consistency":0.521,"epa":133.64,"teamwork":13.9,"teamworkInfo":{"teamwork":13.9,"teamworkSigma":2.96,"teamworkQuality":5.03,"epa":133.64,"consistency":0.521,"matchCount":26},"skills":{"type":"skills_derived","mu":45.72,"sigma":8.33,"score":45.72,"percentile":91.43},"performance":7.4,"performanceInfo":{"mu":16.54,"sigma":3.05,"score":7.4}}},{"_id":"60062_185868","id":"60062_185868","number":"99997A","team_name":"GearMax","organization":"Cuddle STEM Center","location":{"venue":null,"address_1":"","address_2":null,"city":"Zhongshan","region":null,"postcode":"104","country":"Chinese Taipei","coordinates":{"lat":25.1,"lon":121.5}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"consistency":0.532,"epa":108.52,"teamwork":13.79,"teamworkInfo":{"teamwork":13.79,"teamworkSigma":3.01,"teamworkQuality":4.76,"epa":108.52,"consistency":0.532,"matchCount":26},"skills":{"type":"skills_derived","mu":43.03,"sigma":8.33,"score":43.03,"percentile":86.06},"performance":6.7,"performanceInfo":{"mu":15.45,"sigma":2.92,"score":6.7}}},{"_id":"60062_188565","id":"60062_188565","number":"7226A","team_name":"CKJH","organization":"Taichung Municipal Cheng-Kong Junior High School","location":{"venue":null,"address_1":"","address_2":null,"city":"Taichung","region":null,"postcode":"412029","country":"Chinese Taipei","coordinates":{"lat":24.1,"lon":120.7}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":13.73,"teamworkInfo":{"teamwork":13.73,"teamworkSigma":3.45,"teamworkQuality":3.37,"epa":71.09,"consistency":0.508,"matchCount":18},"consistency":0.508,"epa":71.09,"skills":{"type":"skills_derived","mu":39.65,"sigma":8.33,"score":39.65,"percentile":79.29},"performance":6.21,"performanceInfo":{"mu":16.75,"sigma":3.51,"score":6.21}}},{"_id":"60062_188877","id":"60062_188877","number":"88771E","team_name":"XiYuan Go","organization":"臺中市立西苑高級中學附設國中部","location":{"venue":null,"address_1":"","address_2":null,"city":"Taichung","region":null,"postcode":"407","country":"Chinese Taipei","coordinates":{"lat":24.2,"lon":120.6}},"program":{"id":41,"name":"VEX IQ Robotics Competition","code":"VIQRC"},"statiq":{"teamwork":13.61,"teamworkInfo":{"teamwork":13.61,"teamworkSigma":3.42,"teamworkQuality":3.34,"epa":85.61,"consistency":0.396,"matchCount":18},"consistency":0.396,"epa":85.61,"skills":{"type":"skills_derived","mu":46.14,"sigma":8.33,"score":46.14,"percentile":92.28},"performance":5.39,"performanceInfo":{"mu":16.79,"sigma":3.8,"score":5.39}}}],"count":50,"limit":50,"skip":0,"total":718} \ No newline at end of file diff --git a/lib/src/constants.dart b/lib/src/constants.dart index dbb8086..6e65ac8 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -12,6 +12,7 @@ class AppConstants { static const String settingsBox = 'settings_box'; static const String teamHistoryBox = 'team_history_box'; static const String eventHistoryBox = 'event_history_box'; + static const String leaderboardBox = 'leaderboard_cache_box'; // RobotEvents API static const String robotEventsBaseUrl = 'https://www.robotevents.com/api/v2'; diff --git a/lib/src/models/team_model.dart b/lib/src/models/team_model.dart index 9d6b2b8..33bc866 100644 --- a/lib/src/models/team_model.dart +++ b/lib/src/models/team_model.dart @@ -29,7 +29,7 @@ class Team { @HiveField(10) final double? ccwm; @HiveField(11) - final Map? statiq; + final Map? statiq; @HiveField(12) final String? grade; @@ -50,15 +50,22 @@ class Team { }); factory Team.fromJson(Map json) { - final locationObj = json['location'] as Map?; - final city = locationObj?['city'] as String?; - final region = locationObj?['region'] as String?; - final country = locationObj?['country'] as String?; - final locationStr = [city, region, country] - .where((e) => e != null && e.isNotEmpty) - .join(', '); + dynamic loc = json['location']; + String? locationStr; + if (loc is Map) { + final city = loc['city'] as String?; + final region = loc['region'] as String?; + final country = loc['country'] as String?; + locationStr = [city, region, country] + .where((e) => e != null && e.isNotEmpty) + .join(', '); + } else if (loc is String) { + locationStr = loc; + } - final statiq = json['statiq'] as Map?; + final statiqRaw = json['statiq']; + final statiq = + statiqRaw is Map ? Map.from(statiqRaw) : null; final perf = statiq?['performance'] as num?; // Robust ID parsing: @@ -103,7 +110,7 @@ class Team { String? location, double? trueskill, double? ccwm, - Map? statiq, + Map? statiq, String? grade, }) { return Team( diff --git a/lib/src/models/team_model.g.dart b/lib/src/models/team_model.g.dart index 569ea65..8be1b5a 100644 --- a/lib/src/models/team_model.g.dart +++ b/lib/src/models/team_model.g.dart @@ -28,7 +28,7 @@ class TeamAdapter extends TypeAdapter { location: fields[8] as String?, trueskill: fields[9] as double?, ccwm: fields[10] as double?, - statiq: (fields[11] as Map?)?.cast(), + statiq: (fields[11] as Map?)?.cast(), grade: fields[12] as String?, ); } diff --git a/lib/src/repositories/leaderboard_repository.dart b/lib/src/repositories/leaderboard_repository.dart new file mode 100644 index 0000000..afdfc94 --- /dev/null +++ b/lib/src/repositories/leaderboard_repository.dart @@ -0,0 +1,84 @@ +import 'package:roboscout_iq/src/models/team_model.dart'; +import 'package:roboscout_iq/src/services/api_client.dart'; +import 'package:roboscout_iq/src/services/local_db_service.dart'; + +class LeaderboardRepository { + final ApiClient _apiClient; + final LocalDbService _localDb; + + LeaderboardRepository(this._apiClient, this._localDb); + + Future>> getGlobalSkills(String gradeLevel, + {bool forceRefresh = false}) async { + final cacheKey = 'skills_$gradeLevel'; + final box = _localDb.leaderboardBox; + + if (!forceRefresh && box.containsKey(cacheKey)) { + try { + final cachedData = box.get(cacheKey); + if (cachedData is List) { + return cachedData + .map((e) => Map.from(e as Map)) + .toList(); + } + } catch (e) { + print('Error reading skills cache: $e'); + } + } + + try { + final data = await _apiClient.getGlobalSkills(gradeLevel: gradeLevel); + await box.put(cacheKey, data); + return data; + } catch (e) { + if (box.containsKey(cacheKey)) { + final cachedData = box.get(cacheKey); + if (cachedData is List) { + return cachedData + .map((e) => Map.from(e as Map)) + .toList(); + } + } + rethrow; + } + } + + Future> getGlobalTrueSkillRankings( + {bool forceRefresh = false}) async { + const cacheKey = 'trueskill_global'; + final box = _localDb.leaderboardBox; + + if (!forceRefresh && box.containsKey(cacheKey)) { + try { + final cachedData = box.get(cacheKey); + if (cachedData is List) { + return _deserializeTeams(cachedData); + } + } catch (e) { + print('Error reading trueskill cache: $e'); + } + } + + try { + final teams = await _apiClient.getGlobalTrueSkillRankings(); + final jsonList = teams.map((t) => t.toJson()).toList(); + await box.put(cacheKey, jsonList); + return teams; + } catch (e) { + if (box.containsKey(cacheKey)) { + final cachedData = box.get(cacheKey) as List; + return _deserializeTeams(cachedData); + } + rethrow; + } + } + + List _deserializeTeams(List list) { + return list.map((e) { + // Hive returns _Map, need to cast to Map + // for Team.fromJson + final json = Map.from(e as Map); + return Team.fromJson(json); + }).toList(); + } +} diff --git a/lib/src/services/api_client.dart b/lib/src/services/api_client.dart index e0a2f87..ab56eef 100644 --- a/lib/src/services/api_client.dart +++ b/lib/src/services/api_client.dart @@ -305,6 +305,41 @@ class ApiClient { } } + Future> getGlobalTrueSkillRankings( + {String gradeLevel = 'Middle School'}) async { + // RoboStem API uses a different base URL and key + final token = _settings.roboStemApiKey ?? AppConstants.roboStemApiKey; + final dio = Dio(BaseOptions( + baseUrl: AppConstants.roboStemBaseUrl, // https://api.robostem-api.org + headers: { + 'x-api-key': token, + 'accept': 'application/json', + }, + connectTimeout: const Duration(seconds: 30), + receiveTimeout: const Duration(seconds: 30), + )); + + try { + final response = await dio.get('/api/rankings/statiq', queryParameters: { + 'program': 'VIQRC', + 'limit': 100, // Reduced from 2500 for performance + 'grade_level': gradeLevel, + }); + + // RoboStem response structure might differ. Assuming standard list or {data: []} + List> rawList = []; + if (response.data is List) { + rawList = (response.data as List).cast>(); + } else if (response.data is Map && response.data['data'] is List) { + rawList = (response.data['data'] as List).cast>(); + } + + return rawList.map((json) => Team.fromJson(json)).toList(); + } catch (e) { + return []; + } + } + Future> searchTeams( {String? number, int? program, int? limit}) async { // Strategy: Use RobotEvents API for exact team lookup first, diff --git a/lib/src/services/local_db_service.dart b/lib/src/services/local_db_service.dart index 4bf3abb..a6d1867 100644 --- a/lib/src/services/local_db_service.dart +++ b/lib/src/services/local_db_service.dart @@ -39,6 +39,7 @@ class LocalDbService { Hive.openBox('saved_scores'), Hive.openBox(AppConstants.teamHistoryBox), Hive.openBox(AppConstants.eventHistoryBox), + Hive.openBox(AppConstants.leaderboardBox), ]); } @@ -53,6 +54,7 @@ class LocalDbService { Box get teamHistoryBox => Hive.box(AppConstants.teamHistoryBox); Box get eventHistoryBox => Hive.box(AppConstants.eventHistoryBox); + Box get leaderboardBox => Hive.box(AppConstants.leaderboardBox); Future clearAllData() async { await eventsBox.clear(); @@ -63,5 +65,6 @@ class LocalDbService { await scoreEntriesBox.clear(); await teamHistoryBox.clear(); await eventHistoryBox.clear(); + await leaderboardBox.clear(); } } diff --git a/lib/src/state/providers.dart b/lib/src/state/providers.dart index 3d14212..1f7767a 100644 --- a/lib/src/state/providers.dart +++ b/lib/src/state/providers.dart @@ -13,6 +13,7 @@ import 'package:roboscout_iq/src/services/rating_service.dart'; import 'package:roboscout_iq/src/services/secure_storage_service.dart'; import 'package:roboscout_iq/src/services/sync_service.dart'; import 'package:roboscout_iq/src/state/settings_provider.dart'; +import 'package:roboscout_iq/src/repositories/leaderboard_repository.dart'; // Services final localDbServiceProvider = Provider((ref) => LocalDbService()); @@ -62,6 +63,11 @@ final syncServiceProvider = Provider((ref) => SyncService( ref.read(eventsRepositoryProvider), )); +final leaderboardRepositoryProvider = Provider((ref) => LeaderboardRepository( + ref.read(apiClientProvider), + ref.read(localDbServiceProvider), + )); + // Navigation State final bottomNavIndexProvider = StateProvider((ref) => 0); // Default to Favorites diff --git a/lib/src/ui/screens/events_list_screen.dart b/lib/src/ui/screens/events_list_screen.dart index 50566c3..df11de1 100644 --- a/lib/src/ui/screens/events_list_screen.dart +++ b/lib/src/ui/screens/events_list_screen.dart @@ -396,7 +396,7 @@ class _EventsListViewState extends ConsumerState { const SizedBox(width: 8), CupertinoButton( padding: EdgeInsets.zero, - minimumSize: Size.zero, + minSize: 0, child: Icon(CupertinoIcons.clock, color: primaryColor), onPressed: () => _showHistory(context), ), diff --git a/lib/src/ui/screens/world_skills_screen.dart b/lib/src/ui/screens/world_skills_screen.dart index 381b0c5..a1a5538 100644 --- a/lib/src/ui/screens/world_skills_screen.dart +++ b/lib/src/ui/screens/world_skills_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:roboscout_iq/src/models/team_model.dart'; import 'package:roboscout_iq/src/state/providers.dart'; // Standalone sort function for isolate @@ -32,46 +33,71 @@ class _WorldSkillsScreenState extends ConsumerState { // Cache for skills data List> _msSkills = []; List> _esSkills = []; + + // Cache for TrueSkill data (Unified) + List _trueSkills = []; + bool _isLoading = true; String _errorMessage = ''; String _gradeLevel = 'Middle School'; // Default selection + String _metric = 'Skills'; // 'Skills', 'TrueSkill', 'EPA' @override void initState() { super.initState(); - _fetchSkills(); + _fetchData(); } - Future _fetchSkills() async { - setState(() { - _isLoading = true; - _errorMessage = ''; - }); + Future _fetchData( + {bool forceRefresh = false, bool isPullToRefresh = false}) async { + if (!isPullToRefresh) { + setState(() { + _isLoading = true; + _errorMessage = ''; + }); + } try { - final client = ref.read(apiClientProvider); + final repo = ref.read(leaderboardRepositoryProvider); - // Fetch both MS and ES in parallel - final results = await Future.wait([ - client.getGlobalSkills(gradeLevel: 'Middle School'), - client.getGlobalSkills(gradeLevel: 'Elementary School'), - ]); + if (_metric == 'Skills') { + // Fetch both MS and ES in parallel + final results = await Future.wait([ + repo.getGlobalSkills('Middle School', forceRefresh: forceRefresh), + repo.getGlobalSkills('Elementary School', forceRefresh: forceRefresh), + ]); - // Sort in background isolate to prevent UI freeze - // This must happen outside setState - final sortedResults = await Future.wait([ - compute(_sortSkillsList, results[0]), - compute(_sortSkillsList, results[1]), - ]); + // Sort in background isolate to prevent UI freeze + final sortedResults = await Future.wait([ + compute(_sortSkillsList, results[0]), + compute(_sortSkillsList, results[1]), + ]); - if (mounted) { - setState(() { - _msSkills = sortedResults[0]; - _esSkills = sortedResults[1]; + if (mounted) { + setState(() { + _msSkills = sortedResults[0]; + _esSkills = sortedResults[1]; + _isLoading = false; + }); + } + } else if (_metric == 'TrueSkill') { + final results = + await repo.getGlobalTrueSkillRankings(forceRefresh: forceRefresh); - _isLoading = false; - }); + if (mounted) { + setState(() { + _trueSkills = results; + _isLoading = false; + }); + } + } else { + // EPA or other future metrics + if (mounted) { + setState(() { + _isLoading = false; + }); + } } } catch (e) { if (mounted) { @@ -85,7 +111,6 @@ class _WorldSkillsScreenState extends ConsumerState { @override Widget build(BuildContext context) { - final currentList = _gradeLevel == 'Middle School' ? _msSkills : _esSkills; final primaryColor = Theme.of(context).colorScheme.primary; return Material( @@ -94,16 +119,17 @@ class _WorldSkillsScreenState extends ConsumerState { backgroundColor: CupertinoColors.systemGroupedBackground.resolveFrom(context), navigationBar: CupertinoNavigationBar( - middle: const Text('World Skills'), + middle: const Text('World Leaderboards'), trailing: CupertinoButton( padding: EdgeInsets.zero, + onPressed: () => _fetchData(forceRefresh: true), child: Icon(CupertinoIcons.refresh, color: primaryColor), - onPressed: _fetchSkills, ), ), child: SafeArea( child: Column( children: [ + // Metric Selector (Top) Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, vertical: 12.0), @@ -112,57 +138,105 @@ class _WorldSkillsScreenState extends ConsumerState { child: CupertinoSlidingSegmentedControl( thumbColor: primaryColor, backgroundColor: CupertinoColors.tertiarySystemFill, - groupValue: _gradeLevel, + groupValue: _metric, + onValueChanged: (String? value) { + if (value != null) { + setState(() { + _metric = value; + _fetchData(); + }); + } + }, children: { - 'Middle School': Padding( + 'Skills': Padding( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 8), - child: Text('Middle School', + child: Text('Skills', style: TextStyle( fontWeight: FontWeight.w600, - color: _gradeLevel == 'Middle School' + fontSize: 13, + color: _metric == 'Skills' ? Theme.of(context).colorScheme.onPrimary : CupertinoColors.secondaryLabel .resolveFrom(context))), ), - 'Elementary School': Padding( + 'TrueSkill': Padding( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 8), - child: Text('Elementary School', + child: Text('TrueSkill', style: TextStyle( fontWeight: FontWeight.w600, - color: _gradeLevel == 'Elementary School' + fontSize: 13, + color: _metric == 'TrueSkill' + ? Theme.of(context).colorScheme.onPrimary + : CupertinoColors.secondaryLabel + .resolveFrom(context))), + ), + 'EPA': Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 8), + child: Text('EPA', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 13, + color: _metric == 'EPA' ? Theme.of(context).colorScheme.onPrimary : CupertinoColors.secondaryLabel .resolveFrom(context))), ), - }, - onValueChanged: (String? value) { - if (value != null) { - setState(() { - _gradeLevel = value; - }); - } }, ), ), ), + + // Grade Level Selector (Conditionally below metric selector) + if (_metric == 'Skills') + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0) + .copyWith(bottom: 12.0), + child: SizedBox( + width: double.infinity, + child: CupertinoSlidingSegmentedControl( + thumbColor: primaryColor, + backgroundColor: CupertinoColors.tertiarySystemFill, + groupValue: _gradeLevel, + onValueChanged: (String? value) { + if (value != null) { + setState(() { + _gradeLevel = value; + }); + } + }, + children: { + 'Middle School': Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 8), + child: Text('Middle School', + style: TextStyle( + fontWeight: FontWeight.w600, + color: _gradeLevel == 'Middle School' + ? Theme.of(context).colorScheme.onPrimary + : CupertinoColors.secondaryLabel + .resolveFrom(context))), + ), + 'Elementary School': Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 8), + child: Text('Elementary School', + style: TextStyle( + fontWeight: FontWeight.w600, + color: _gradeLevel == 'Elementary School' + ? Theme.of(context).colorScheme.onPrimary + : CupertinoColors.secondaryLabel + .resolveFrom(context))), + ), + }, + ), + ), + ), + Expanded( - child: _isLoading - ? const Center(child: CupertinoActivityIndicator()) - : _errorMessage.isNotEmpty - ? Center(child: Text('Error: $_errorMessage')) - : currentList.isEmpty - ? const Center(child: Text('No data found')) - : ListView.builder( - padding: EdgeInsets.zero, - // Fixed extent optimization for smooth scrolling - itemExtent: 72.0, - itemCount: currentList.length, - itemBuilder: (context, index) => - _buildSkillTile( - currentList[index], context), - ), + child: _buildContent(context), ), ], ), @@ -171,10 +245,71 @@ class _WorldSkillsScreenState extends ConsumerState { ); } + Widget _buildContent(BuildContext context) { + if (_isLoading) { + return const Center(child: CupertinoActivityIndicator()); + } + + if (_errorMessage.isNotEmpty) { + return Center(child: Text('Error: $_errorMessage')); + } + + if (_metric == 'EPA') { + return const Center(child: Text('Coming soon')); + } + + List slivers = []; + + // Add Pull-to-Refresh + slivers.add(CupertinoSliverRefreshControl( + onRefresh: () => _fetchData(forceRefresh: true, isPullToRefresh: true), + )); + + if (_metric == 'Skills') { + final currentList = + _gradeLevel == 'Middle School' ? _msSkills : _esSkills; + if (currentList.isEmpty) { + slivers.add(const SliverFillRemaining( + child: Center(child: Text('No data found')), + )); + } else { + slivers.add(SliverFixedExtentList( + itemExtent: 72.0, + delegate: SliverChildBuilderDelegate( + (context, index) => _buildSkillTile(currentList[index], context), + childCount: currentList.length, + ), + )); + } + } else if (_metric == 'TrueSkill') { + final currentList = _trueSkills; + if (currentList.isEmpty) { + slivers.add(const SliverFillRemaining( + child: Center(child: Text('No data found')), + )); + } else { + slivers.add(SliverFixedExtentList( + itemExtent: 72.0, + delegate: SliverChildBuilderDelegate( + (context, index) => + _buildTrueSkillTile(currentList[index], index + 1, context), + childCount: currentList.length, + ), + )); + } + } + + return CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: slivers, + ); + } + Widget _buildSkillTile(Map item, BuildContext context) { final rank = item['rank']; - final team = item['team'] as Map; - final number = team['number']; + final teamRaw = item['team']; + final team = teamRaw is Map ? Map.from(teamRaw) : {}; + final number = team['number'] ?? ''; final name = team['name'] ?? ''; final score = item['score']; final prog = item['programming']; @@ -259,7 +394,7 @@ class _WorldSkillsScreenState extends ConsumerState { Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( - color: primaryColor.withOpacity(0.15), + color: primaryColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(20), ), child: Text( @@ -279,4 +414,113 @@ class _WorldSkillsScreenState extends ConsumerState { ), ); } + + Widget _buildTrueSkillTile(Team team, int rank, BuildContext context) { + final number = team.number; + final name = team.name; + // According to memory, 'teamwork' maps to trueskill rating (Mu) + final score = team.statiq?['teamwork'] ?? 0.0; + final primaryColor = Theme.of(context).colorScheme.primary; + + return GestureDetector( + onTap: () { + ref.read(teamSearchQueryProvider.notifier).state = number; + ref.read(bottomNavIndexProvider.notifier).state = 2; + }, + child: Container( + height: 72, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: CupertinoColors.secondarySystemGroupedBackground + .resolveFrom(context), + border: Border( + bottom: BorderSide( + color: CupertinoColors.separator.resolveFrom(context), + width: 0.5, + ), + ), + ), + child: Row( + children: [ + // Rank Circle + Container( + width: 32, + height: 32, + alignment: Alignment.center, + decoration: BoxDecoration( + color: CupertinoColors.systemGroupedBackground + .resolveFrom(context), + shape: BoxShape.circle, + ), + child: Text('$rank', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: CupertinoColors.label.resolveFrom(context))), + ), + const SizedBox(width: 12), + // Team Info + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text(number, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 17, + color: + CupertinoColors.label.resolveFrom(context))), + const SizedBox(width: 8), + Expanded( + child: Text( + name, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16, + color: CupertinoColors.label.resolveFrom(context), + ), + ), + ), + ], + ), + const SizedBox(height: 2), // Minimal gap + // TrueSkill doesn't have Prog/Driver breakdown usually displayed here + // Maybe display organization or location? + Text(team.organization ?? '', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13, + color: CupertinoColors.secondaryLabel + .resolveFrom(context))), + ], + ), + ), + const SizedBox(width: 12), + // Score Pill + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: primaryColor.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + (score as num).toStringAsFixed(2), + style: TextStyle( + fontWeight: FontWeight.w800, + color: primaryColor, + fontSize: 16, // Slightly smaller for decimals + ), + ), + ), + const SizedBox(width: 4), + const Icon(CupertinoIcons.chevron_right, + size: 14, color: CupertinoColors.systemGrey3), + ], + ), + ), + ); + } }